引言
首先问几个问题?
- 为什么要有KMP算法?
- 相对于朴素算法的过程,KMP算法的过程有哪些不同?
- 什么是字符串的前缀和后缀,它到底有啥用?
- KMP算法中的next数组是什么?为什么要用next数组?
- next数组的求解过程?怎样用代码实现出来?(★重点,难点★)
- KMP算法怎样用代码实现?
我认为只要弄清楚上面的几个问题,KMP算法也就迎刃而解了。
本文就从上面几个问题一个个的铺开
一、为什么要有KMP算法?
在字符串中有一类问题是从查找子串问题,假如现在有一个主串(较长一些)和一个子串(较短一些)。我想在主串中进行查找,看看主串中是否包含了子串。
比如主串为 aaaab .。子串为aab 。 问主串中是否包含子串? 答案是显而易见的,主串中包含子串。
这就出现了一种朴素算法,用来查找主串中是否包含子串。
在说朴素算法的实现过程中, 说一个约定:字符串下标都是从0开始的
朴素算法的具体过程:
- 假设现在有一个主串 aaaab,子串 aab。i 代表当前指向主串的字符位置。j 代表当前指向子串的字符位置
- 从i=0 ,j=0 开始。这时候第一个字符相等,那就i++,j++ 。比较第二个字符,经比较,第二个字符也相等,那就i++,j++,去比较第三个字符。
- 当i=2,j=2时,发现不相等,那就子串向后移动一个位置,重新开始比较。子串向后移动一个位置的意思是在下面的比较过程中,字串的 j 仍然从0开始比较,但主串的 i 从 1开始比较。
- 重点来了!!!因为第一次主串和子串没有匹配上,因此要进行回溯(就是 j 重新赋值 0。i从上一个位置的后一个位置开始。i=i-j+1。如果不是很理解前面这个式子,可以先放放,等会再回来看)。
- 所以朴素算法的思想就是:一个个进行对比,如果发现不匹配,那就进行回溯(也就是相对于主串,子串向后移动一个单位,从子串的第一位置开始,重新开始对比子串。),直到成功查找到子串或者子串移动到最后也没匹配成功。
为什么要有KMP算法?
答案: 因为上面的对比过程效率比较低,每次对比匹配失败后,i 和 j 都要进行回溯,浪费了好多时间,因此KMP就创造出来了。
二、相对于朴素算法,KMP算法有哪些不同?
首先KMP用代码实现的过程就写了一个while循环,next数组的求解过程也写了一个while循环,就求出来了。没有什么难的地方,重心要放到理解next数组的理解KMP算法过程上。
再说一下朴素算法的大概过程: 拿着子串,一个个的字符和主串进行对比,一旦对比失败,子串后移一个单位,i 和 j 进行回溯(j回溯到0),重新从字串的第一个字符的位置开始对比。
相对于朴素算法,kMP算法的过程是这样的:
拿着子串,一个个的字符去和主串进行对比,一旦对比失败,子串仍然后移(但不一定是一个单位),而且 i 不进行回溯。j 进行回溯,但是并不像朴素算法那样直接j=0,而是回溯到一个其他的地方。(重要!!!)
上面就是KMP算法的大概过程,与朴素算法不一样的地方就是:子串后移的不是一个单位,i不进行回溯,j虽然进行回溯,但并不是回溯到j=0的位置。
对于上面说的,你现在可以不知道子串后移几个单位,j回溯到那个位置,但是你一定要知道KMP的大概过程是啥, 能达到自己不看上面说的,能口述出来的那种(★★★)
三、什么是字符串的前缀和后缀,它到底有啥用?
下面举两个个例子来说明字符串的前缀和后缀
假如现在有一个字符串为aba。
那么他的前缀字符串(前缀字符串的长度最长要比原字符串少1)分别为:a,ab
他的后缀字符串(后缀字符串的长度最长要比原字符串少1)分别为:a,ba。
再来一个例子abababa。
前缀字符串有:a,ab , aba , abab,ababa,ababab
后缀字符串有:a ,ba , aba , baba,ababa,bababa.
截至到现在:你可以不知道前缀和后缀的作用,但是你一定要会求一个字符串有哪些前缀串和后缀串!!!
每个字符串(不包括空串)都有自己的前缀串和后缀串,而且每个字符串都有长度,通过对比前缀串和后缀串,我们就能得到一个最大长度,这个长度是指在所有的前缀串中选一个,在所有的后缀串中选一个,而且这俩串相等(可能会有多个相等的串),长度最长。这就被称为最长前缀后缀串。
通过上面一段话,你要会求一个字符串的最长前缀后缀串!!!
下面说几个例子:
比如上面的字符串aba,他的最长前缀后缀串就是: a
比如上面的字符串abababa,他的最长前缀后缀串就是: ababa
下面说他的作用:
假如现在要在 主串 abababc 中查找 子串 ababc ,如图所示
注意我上面图片的文字。上面的子串为 ababc, 这里咱们把他分成三部分,第一部为为第一个 ab(下标为0和1)。 第二部分为第二个ab(下标为2和3),第三部分为 c(下标为4)。
因为我们在匹配过程中,我们匹配失败,所以要把子串后移,那么后移几个单位合适呢?我们 i 是不进行回溯的(这事KMP的特点,如果你想问 i 不进行回溯,能保证匹配的正确性吗?那就继续看下去吧),j 进行回溯,那么j要回溯几个单位合适呢?
考虑上面两个问题之前,你要记住咱们有两个已知条件
- 字符c之前的四个字符我们都匹配成功了。
- 字符串abab的最长前缀后缀字符串为ab。
根据上面的两条结论,我们先看第二个ab(它既是字符串abab的最长前缀后缀,而且他和主串都匹配成功了),通过上面的两个已知条件,你是不是发现了什么联系?
他们的联系就是:假如我们子串向后移动两个距离,那么字符的第一个ab就和移动之前的第二个ab在主串的对应位置是一样的(主串不会动)。那么这时候子串的第一个位置和第二个位置就不用匹配了,因为之前我们匹配过了,而且匹配的是成功的(利用的等价的关系,一开始字符串的后缀和主串匹配,字符串的后缀和字符串的前缀还相等,那就意味着字符串的前缀和主串对应的位置相等)
截止到现在位置:你应该知道字串的最长前缀后缀是干啥的了,如果你搞懂了上面那个问题,下面的next数据就不然理解了。重要的事情说三遍!!! 一定要搞懂上面那个问题 !一定要搞懂上面那个问题!一定要搞懂上面那个问题!
我认为上面那个问题是这篇文章最重要的地方,一定要先去理解他,再继续看下面的!!!
四、KMP算法中的next数组是什么?为什么要用next数组?
首先next数组就是和上面的最大前缀后缀对应的,一定要理解上面最大前缀后缀的作用之后,再看下面的next数组的定义
不要仅仅只知道最大前缀后缀的计算过程,一定要理解他的作用所在,不然就不知道为什么我们要引入next数组
next数组定义:
代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀,并且规定next[0]=-1。
举例说明:
字符串abababc(下标从0开始)
那么next[]={-1,0,0,1,2,3,4};
比如要计算next[1],根据定义可得,他前面的字符串为 a,这个字符串就没有前缀和后缀(前缀和后缀的长度要比原字符串少1). 比如要计算next[4].它前面的字符串为 abab,最长前缀后缀就是 ab,因此next[4]=2.
上面定义需要注意的地方就是,next[0]=-1,如果计算next [ j ],那就是看下标 j 前面的所有字符,并不包括下标 j 这个字符。
下面再说说前面两个为解决的问题
- KMP算法中,如果匹配失败,j 应该回溯到哪个位置?
- KMP算法中,如果匹配失败,那字串应该向后移动几个单位
这里给出结论,如果匹配失败,j 应该回溯到 next[j] 这个位置
原因:因为在下标j这个位置,他的next[j]代表他的 j 字符之前的字符串最长前缀后缀的大小,那么如果 j 回溯到了next[j],这个位置,代表他 j 之前的字符仍然不用进行匹配,这就保证了字符匹配的正确性
因为字符 j 回溯了,而且接下来匹配的时候仍然是 i(没变) 和 j(回溯过了) 继续进行匹配,那么子串就要后移,才能在同一条线上继续匹配,当然这个问题在代码中体现不出来,在画图中可以体现出来,下面看图片
如上图所示,i 是不需要进行回溯的,j要进行回溯,回溯到j=next [ j ] 的位置,至于子串的后移操作知识方便理解,在代码中不用体现出来。
五、next数组的求解过程?
这是本篇文章的第五个问题了,你可以先问问自己,前四个问题是否搞清楚了。
这个求解过程,我们来拿例子讲解
比如我们现在有一个字符串 ababcabab
根据next数组的定义,我们手算可以得出next [ ] ={-1,0,0,1,2,0,1,2,3};
假如我们现在知道了next[0]~next[3](手算出来的)。想去利用代码去求next [ 4 ]。 那下面我们分析一下怎么求
因为我们已经知道了next[3]=1; 下标3 之前的字符串为 aba,最大前缀后缀为1,就是第一个a(下标为0)和第3个a(下标为2)相等。利用这个已知条件,我们去求解next[4]。 相对于下标3前面的字符串aba,下标4之前的字符串为abab,
在最后多了一个b(下标为3),那么我们可以这样想,因为在next[3]中存在一个最长前缀后缀,那么我们求next[4]是,可以把前面的最长前缀后缀各自再加上一个字符(上面的例子中,aba的前缀是a(下标为0),后缀的为a(下标为2),前缀再加一个字符就是ab(下标为0和1),后缀再加一个字符就是ab(下标为2和3))。那么我们只需要比较新加的一个字符是否相等是不是就可以了?,这样就分为两种情况
- 刚好二者新加的一个字符相等,那么next[j]=next[j-1]+1;
- 如果二者新加的一个字符不相等,那该怎么办?
下面我们再看一个新的例子:
字符串为 abaababa
手算可以求得next[]={-1,0,0,1,1,2,3,2};
假设我们现在已经知道了next[6]=2,我们想用代码的方式根据next[6]求出来next[7]是多少?
我们再用上面的方法分析一遍,看看怎样根据next[6]求出来next[7].
下标6前面字符串为abaaba,最大前缀和后缀字符串为 aba,下标为7前面的字符串为abaabab。如果想求下标为7的前面字符串的最大前缀后缀,那就在下标为6的最大前缀后缀后面分别一个字符,那前缀就变成了abaa,后缀变成了abab,通过对比新加的一个字符,发现并不相等,那接下来该怎么办呢?
我们在重新理一下我们的需求,我们想求next[7],下标为7之前的字符串为abaabab。对比前缀新加的字符a(下标为3)和后缀新加的 b(下标为6)是发现不相等,那么我还是要求最大前缀后缀长,因为前不相等的原因,那这个next[7]肯定不能像相等的情况(next[6]=next[5]+1)来算,所以前缀肯定要缩小(自己可以想想为什么?),这个缩小是指在添加一个新字符的基础上进行缩小。
现在先看几个图片,再继续往下面分析
看完上面图片后,我们开始关注j=3这个下标,我们现在是想在字符串abaa(下标为0-3)中找到一个前缀字符串他等于在我在字符串abab(下标为3-6)中找到一个后缀字符串,而且要长度最大的那种,我们现在还有一个已知条件,那就这字符串abaa和字符串abab的前3个字符是相等的(这是根据next[6]=3推出来的)。
我们现在在讨论一下j=1这个位置,这个因为next[3]=1,假如我们的字符串下标为1的字符和下标为6的字符相等,我们能说这个next[3]+1=2(因为字符串是从下标0开始的)就是这时候的能说这就next[7]的值吗? 答案是可以
原因:先看next[3]=1.这说明下标3之前的字符串aba的最长前缀后缀为1,就说明下标1之前的字符串和下标3之前的字符串相等,这两个相等的字符串长度为next[3]=1.同时看next[6]=3,这说明下标3之前的字符串和下标6之前字符串相等。根据对称性原理,我们就能说next[3]+1=2就是next[7]的值。
上面说的是相等的情况,如果不相等怎么办,那就继续向前找,假如这里下标为1和下标为6不相等,那下标1就变成next[1].
按照上面的规律,一直到,知道找到或者找到了头还有找到与之匹配的,那就结束(写代码时控制好边界)。
其实这个一直找的过程就是递归的过程(这地方不太好理解,可以多想想)
下面再看一个网上的图片(图片链接为:点击这里 如果有冒犯版权,我会及时删除 )
根据上面的过程,我们可以得知如果我们知道了next[0]-next[j-1]。我们就能得出来next[j](递推求解)
下面具体看代码:
//计算next数组函数 void Next(int next[], string s) { int j = 0; int k = -1; int len = s.length(); next[0] = -1;//这个是规定好的 while (j < len-1 ) { if ((k == -1) || (s[k] == s[j])) { k++; j++; next[j] = k; } else { k = next[k];//递归过程 } } }
六、KMP算法怎样用代码实现
只要求出来next数组,再根据KMP的求解过程,那么KMP用代码实现就很容易得出来了,下面看代码
代码
//KMP搜索 int KMPSearch(string str, string s,int next[]) { int i = 0, j = 0; int strl = str.length(); int sl = s.length(); while (i < strl && j < sl) { if (j == -1 || str[i] == s[j]) { i++; j++; } else { j = next[j];//j回溯的位置,i不进行回溯 } } if (j == sl)//查询到子串s return i - j; else //未查询到 return -1; }
创作不易,如果您看到了最后,何不点个赞再走呢!!!