字符串匹配——KMP算法

​ 字符串匹配是计算机编程中最常使用到的基础算法之一。字符串匹配相关的算法很多,Knuth-Morris-PrattKMP)算法是最常用的之一。最近在学习KMP算法,学习了许多相关的博客,记录一下,以备日后不会写了回来看看。

KMP算法有两个要点:1)部分匹配 和next数组的计算;2)利用部分匹配表解决字符串匹配问题。

1、KMP算法原理

(1)原理

​ 给定两个字符串:文本串S="BBC ABCDAB ABCDABCDABDE"和模式串P="ABCDABD",要求找出模式串P是否是文本串S的子串。KMP算法解决这一问题的原理如下:

1)首先,文本串S="BBC ABCDAB ABCDABCDABDE"的第一个字符与模式串P="ABCDABD"的第一个字符进行比较。因为B与A不匹配,所以搜索词后移一位。

2)B与A不匹配,搜索词再往后移。

3)重复2)的操作,直到文本串有一个字符,与模式串的第一个字符相同为止。

4)比较文本串和模式串的下一个字符,还是相同。

5)重复4)的操作,直到文本串有一个字符,与模式串对应的字符不相同。或者到模式串的最后一个字符都相同为止。

6)此时,最自然的反应是将模式串整个后移一位,再从头逐个比较。这样的操作便是暴力枚举的方法,但是效率很差,因为需要把"对比位置"移到已经比较过的位置,重比一遍。

7)一个基本事实是,当空格D不匹配时,其实前面六个字符是"ABCDAB"KMP算法的想法是,设法利用这个已知信息,不把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了匹配效率。

8)那么如何保证搜索位置继续往后移的时候不会漏掉能够匹配成功的子串呢?可以针对模式串,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

搜索词 A B C D A B D
部分匹配值 0 0 0 0 1 2 0

根据部分匹配值表可以获得模式串的next值数组。next 数组考虑的是除当前字符外的最长相同前缀后缀,将部分匹配值整体右移一位,然后初值赋为-1,如下表所示:

搜索词 A B C D A B D
next -1 0 0 0 0 1 2

9)已知空格D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
移动位数=已匹配的字符数−对应的部分匹配值移动位数 = 已匹配的字符数 - 对应的部分匹配值 移动位数=已匹配的字符数−对应的部分匹配值
移动位数=6-2=4,因此模式串向后移动4位,继续匹配。上面的公式等价于:
移动位数=j−next[j]也等价于j=next[j]移动位数=j-next[j]\\ 也等价于\\ j=next[j] 移动位数=j−next[j]也等价于j=next[j]
其中j为模式串中的匹配失败的字符下标。

10)因为空格不匹配,模式串还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将模式串向后移2位。

11)因为空格A不匹配,继续后移一位。

12)逐位比较,直到发现CD不匹配。于是,移动位数 = 6 - 2,继续将模式串向后移动4位。

13)逐位比较,直到模式串的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将模式串向后移动7位,这里就不再重复了。

(2)C++代码

// KMP字符串匹配,函数返回一个数组,数组前n-1个元素是匹配成功的子串的起始位置,最后一个元素是匹配成功的子串数目
// 函数两个字符串参数,分别是文本字符串和模式串
vector<int> kmpSearch(string s,string p)
{int i=0,j=0;int lenS=s.length();int lenP=p.length();int cnt=0;vector<int> res;while(i<lenS && j<lenP){    //如果j==-1或者匹配成功,继续匹配下一个字符if(j==-1 || s[i]==p[j]){i++;j++;}// 如果j!=-1并且匹配失败,i保持不变,j=next[j],next[j]是模式串第j+1个元素的next值// 这相当于是文本串不变,模式串向右移动j-next[j]else{j=next[j];}if(j==lenP){cnt++;res.push_back(i-j);j=next[j];}}res.push_back(cnt);return res;
}

2、部分匹配表和next数组的计算

(1)原理

​ 首先,要了解两个概念:“前缀"和"后缀”。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。例如对于前文的模式串P=“ABCDABD”:

前缀:A、AB、ABC、ABCD、ABCDA、ABCDAB

后缀:D、BD、ABD、DABD、CDABD、BCDABD

模式串P="ABCDABD"的部分匹配表:

搜索词 A B C D A B D
部分匹配值 0 0 0 0 1 2 0

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。

上表是这样得到的:

  • "A"的前缀和后缀都为空集,共有元素的长度为0

  • "AB"的前缀为[A],后缀为[B],共有元素的长度为0

  • "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0

  • "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0

  • "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1

  • "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2

  • "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0

使用递推计算next:

对于P的前j+1个序列字符:

  • p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1
  • p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] = next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。 相当于在字符p[j+1]之前不存在长度为k+1的前缀"p0 p1, …, pk-1 pk"跟后缀“pj-k pj-k+1, …, pj-1 pj"相等,那么是否可能存在另一个值t+1 < k+1,使得长度更小的前缀 “p0 p1, …, pt-1 pt” 等于长度更小的后缀“pj-t pj-t+1, …, pj-1 pj”呢?如果存在,那么这个t+1 便是next[ j+1]的值,此相当于利用已经求得的next 数组(next [0, ..., k, ..., j])进行P串前缀跟P串后缀的匹配。

(2)C++代码

vector<int> nextArrayGet(string p)
{int lenP=p.length();int k=-1;int j=0;vector<int> next;next.push_back(-1);while(j<lenP-1){//p[k]表示前缀,p[j]表示后缀if(k==-1 || p[k]==p[j]){j++;k++;next.push_back(k);}else{k=next[k];}}return next;
}

(3)next数组的优化

前面使用的next数组还存在一个小问题。这个问题不影响使用,但是依然存在无效的操作。

如果用之前的next 数组方法求模式串“abab”next 数组,可得其next 数组为{-1, 0, 0, 1},当它跟下图中的文本串去匹配的时候,发现bc失配,于是模式串右移j - next[j] = 3 - 1 =2位。

右移2位后,b又跟c失配。事实上,因为在上一步的匹配中,已经得知p[3] = b,与s[3] = c失配,而右移两位之后,让p[ next[3] ] = p[1] = b 再跟s[3]匹配时,必然失配。

问题在于不该出现p[j] = p[ next[j] ]。因为当p[j] != s[i] 时,下次匹配必然是p[ next [j]]s[i]匹配,如果p[j] = p[ next[j] ],必然导致后一步匹配失败(因为p[j]已经跟s[i]失配,然后还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很显然,必然失配),所以不能允许p[j] = p[ next[j ]]。如果出现了p[j] = p[ next[j] ]怎么办呢?如果出现了,则需要再次递归,即令next[j] = next[ next[j] ]

vector<int> nextArrayGet(string p)
{int lenP=p.length();int k=-1;int j=0;vector<int> next;int res[lenP];res[0]=-1;while(j<lenP-1){//p[k]表示前缀,p[j]表示后缀if(k==-1 || p[k]==p[j]){j++;k++;//较之前next数组求法,改动在下面4行// p[j] != p[ next[j] ]时,和原来一样if(p[j]!=p[k])res[j]=k;       //之前只有这一行//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]elseres[j]=res[k];}else{k=res[k];}}for(int i=0;i<lenP;i++)next.push_back(res[i]);return next;
}

使用优化之后的next数组为{-1, 0, -1, 0},进行匹配:

1)S[3]P[3]匹配失败。

2)S[3]保持不变,P的下一个匹配位置是P[next[3]],而next[3]=0,所以P[next[3]]=P[0]S[3]匹配。

3)由于上一步骤中P[0]S[3]还是不匹配。此时i=3,j=next [0]=-1,由于满足条件j==-1,所以执行“++i, ++j”,即主串指针下移一个位置,P[0]S[4]开始匹配。最后j==lenP,跳出循环,输出结果i - j = 4(即模式串第一次在文本串中出现的位置),匹配成功,算法结束。

3、KMP算法字符串匹配demo

#include<iostream>
#include<vector>
#include<string>
using namespace std;//next数组计算
vector<int> nextArrayGet(string p)
{int lenP=p.length();int k=-1;int j=0;vector<int> next;int res[lenP];res[0]=-1;while(j<lenP-1){//p[k]表示前缀,p[j]表示后缀if(k==-1 || p[k]==p[j]){j++;k++;//较之前next数组求法,改动在下面4行// p[j] != p[ next[j] ]时,和原来一样if(p[j]!=p[k])res[j]=k;       //之前只有这一行//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]elseres[j]=res[k];}else{k=res[k];}}for(int i=0;i<lenP;i++)next.push_back(res[i]);return next;
}//KMP匹配
// KMP字符串匹配,函数返回一个数组,数组前n-1个元素是匹配成功的子串的起始位置,最后一个元素是匹配成功的子串数目
// 函数两个字符串参数,分别是文本字符串和模式串
vector<int> kmpSearch(string s,string p)
{int i=0,j=0;int lenS=s.length();int lenP=p.length();int cnt=0;vector<int> res;//计算next数组vector<int> next=nextArrayGet(p);while(i<lenS && j<lenP){  //如果j==-1或者匹配成功,继续匹配下一个字符if(j==-1 || s[i]==p[j]){i++;j++;}// 如果j!=-1并且匹配失败,i保持不变,j=next[j],next[j]是模式串第j+1个元素的next值// 这相当于是文本串不变,模式串向右移动j-next[j]else{j=next[j];}if(j==lenP){cnt++;res.push_back(i-j);j=next[j];}}res.push_back(cnt);return res;
}//主函数
int main()
{string s, p;cout<<"输入文本串:"<<endl;cin>>s;cout<<"输入模式串:"<<endl;cin>>p;vector<int> res=kmpSearch(s,p);if(res[res.size()-1]==0)cout<<"匹配失败\n";else{cout<<"匹配成功,文本串中存在 "<<res[res.size()-1]<<" 个模式串子串,所在位置分别为:\n";for(int i=0;i<res[res.size()-1];i++)cout<<res[i]<<endl;}return 0;
}

字符串匹配——KMP算法相关推荐

  1. kmp算法详解php,php中字符串匹配KMP算法实现例子

    KMP算法是一个比较高级的算法了,加了改进了,下面我们来在php中实现KMP算法,希望例子对各位同学会带来帮助哦. kmp算法是一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J. ...

  2. C++实现字符串匹配KMP算法

    文章目录 1. 概述 2. 代码实现 3. 代码测试 1. 概述 Kmp算法的介绍及思想参阅下面两篇文章: 字符串匹配KMP算法 算法)通俗易懂的字符串匹配KMP算法及求next值算法 2. 代码实现 ...

  3. 字符串匹配 KMP算法

    问题描述:字符串匹配即查找待匹配字符串(模式串)p在主串s中的位置.一般处理这种问题往往采用简单粗暴的方法--暴力匹配法.所谓暴力匹配法,就是对主串s的每一个字符与要匹配的字符串p的每个字符进行逐一匹 ...

  4. 字符串匹配KMP算法

    字符串匹配KMP KMP过程其实就是去找下一个更好的状态的过程,省略去了中间穷举的无用过程,直接跳到下一个更好的状态,通过模式串本身的信息,站在模式串的角度来考虑问题 取长的一对 若想让模式串直接从S ...

  5. C语言实现字符串匹配KMP算法

    相信很多人(包括自己)初识KMP算法的时候始终是丈二和尚摸不着头脑,要么完全不知所云,要么看不懂书上的解释,要么自己觉得好像心里了解KMP算法的意思,却说不出个究竟,所谓知其然不知其所以然是也. 字符 ...

  6. 【数据结构与算法】字符串匹配 KMP 算法

    单模式串匹配 BF 算法和 RK 算法 BM 算法和 KMP 算法 多模式串匹配算法 Trie 树和 AC 自动机 KMP 算法 KMP 算法是根据三位作者(D.E.Knuth,J.H.Morris ...

  7. 字符串匹配KMP算法的讲解C++

    转自http://blog.csdn.net/starstar1992/article/details/54913261 也可以参考http://blog.csdn.net/liu940204/art ...

  8. 字符串匹配KMP算法的理解(详细)

    1. 引言 本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱导致写也写得混乱.所以一直想找机会重新写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有修改本文. ...

  9. 字符串匹配——KMP算法【C语言】

    KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法).KMP算法的核心是利用匹配失败后 ...

最新文章

  1. 【三代增强干货一枚】外向交货单Delivery (VL01N)Header屏幕增强
  2. boost::mpi模块对gather() 和gatherv() 集合的测试
  3. 省、省、省!!!企业如何搭建易用性网络
  4. 【云栖大会】站在时间轴上看基础设施
  5. 工厂供电MATLAB仿真,工厂供电课程设计---基于MATLAB的电力电子系统仿真
  6. 东大OJ-1588: Routing Table
  7. LINUX上用Robot截屏得到全黑之分析
  8. 重装系统蓝屏,电脑开机蓝屏解决方法记录
  9. 财报汇总 | 中国生物制药、格林酒店、海亮教育、洪恩教育等5家企业公布最新业绩...
  10. 如何用mysql创建orders表_MySQL学习十四创建和操纵表
  11. Pycharm 报错 Environment location directory is not empty的解决方法
  12. SecureCRT和乱码
  13. Flutter淘宝App之首页聚划算倒计时的实现
  14. 使用urllib.request库获取cookie登录
  15. 聊聊高考分数线和选择
  16. 羊驼alpaca php,“草泥马”-----羊驼(Alpaca)
  17. 平凡世界中每一个平凡人
  18. 关于对于c语言全局变量初始化问题
  19. c语言qq加密具体思路,悄悄告诉你:C语言如何实现QQ密码大盗
  20. 酒店订房系统 jsp+ssm+mysql项目

热门文章

  1. 【安全】整合spring security
  2. 微营销好标题之金玉良言(微营销标题吸引粉丝篇-4)
  3. Android 虹软SDK人脸识别图文使用说明(更新:2019.5.14)
  4. [算法课]算法考试复习范围全解
  5. 其实,成功离我们并不远!
  6. 在线教育业务笔记03
  7. 卡塞格林光学系统_卡塞格林系统光学装调技术研究
  8. 《Java核心面试题库》.txt
  9. jdk-9.0.4安装与环境变量配置
  10. 数值运算pythonmopn_数的解释|数的意思|汉典“数”字的基本解释