核心1 next数组的引入
此文章有360行,阅读大约需要10分钟,细读约须20分钟。
KMP算法相比BF算法,最大的特点就是在匹配的过程中,不需要回溯主串的指针i,且减少不必要的子串对比。
比如下面的例子:
S:same123same123same123same6
T:same123same6
按照BF算法,从s开始比对子串,比对a ,比对m… …比到6的时候发现不一样
S:same123same123same123same6
T:same123same6
|
接着我们回溯子串 i 和 j( i 为主串指针,j 为查找串指针),一个一个挪,比较:
S:same123same123same123same6
T: same123same6
|
S:same123same123same123same6
T: same123same6
|
S:same123same123same123same6
T: same123same6
|
.
.
.
S:same123same123same123same6
T: same123same6
|
到这里,从s开始比对子串,比对a ,比对m… …比到6的时候发现不一样
S:same123same123same123same6
T: same123same6
|
接着我们再回溯指针 i 和 j,再一个一个挪,比较:
S:same123same123same123same6
T: same123same6
|
S:same123same123same123same6
T: same123same6
|
.
.
.
S:same123same123same123same6
T: same123same6
自此,查找完毕
下面我们根据KMP算法查找:
S:same123same123same123same6
T:same123same6
按照KMP算法,从s开始比较,比到6的时候发现不一样
S:same123same123same123same6
T:same123same6
|
i 指针不动,j 指针回溯到子串same后面1 的位置:
S:same123same123same123same6
T: same123same6
|
从1(注意了,从1开始)开始比较,比到6的时候发现不一样
S:same123same123same123same6
T: same123same6
|
i 指针不动,j 指针回溯到子串same后面1 的位置
S:same123same123same123same6
T: same123same6
|
很快地,子串便已经查找到,区别十分明显!
代码如下:
int KMP(int start,har T[],har S[])
{
int i=start,j=0;
while(S[i]!='\0'&&T[j]!='\0')
{
if(j==-1||T[i]==S[j])
{
i++; //继续对下一个字符比较
j++; //模式串向右滑动
}
else j=next[j]; // i 指针不动,j 指针回溯
}
if(T[j]=='\0') return (i-j); //匹配成功返回下标
else return -1; //匹配失败返回-1
}
先别管为什么 j 指针回溯到next数组保存的值的位置,现在的问题是,怎么知道 j 指针回溯到哪里呢?
首先,我们把这个回溯的位置记个符号为 k
由上面例子可以看出,子串的same和主串中的下一个same是对齐的。
S:same123same123same123same6
T: same123same6
|||| (对齐)
S:same123same123same123same6
T: same123same6
|||| (对齐)
(注意,是same对齐,不是same123same对齐,i 指针只指到1,1和之后的还是要比较的)
j 是回溯到 子串前面的same 的后面,因为子串前面的same和后面的same是一模一样的。
也就是说,k的位置是在 S串中 出现 前后出现相等的字符串 (最长)(分别起名 Sa 和 Sb )的情况下,Sa 的后面。比如,上面的例子same 就是 Sa。
至于为什么要回溯到Sa的后面,下面举例解释一下。
我们把上面的例子中的子串前面的same命名为Sa,后面的same命名为Sb,有Sa == Sb,则:
比到6的时候,T[i]前面的和S[j]前面的都相等,即S串的same123same == Sa123Sb
S:same123same123same123same6
T: Sa 123 Sb 6
|
所以
S: Sa 123 Sb 123same123same6
T: Sa 123 Sb 6
|
i 不动,j 进行回溯:
S: Sa 123 Sb 123same123same6
T: Sa 123 Sb 6
|
又因为Sa == Sb,所以Sa不用比对都知道相等。
所以回溯到Sa后面是正确的,不影响判断,不会漏判
当然,这只是一个例子,要解释所有情况还是要用数学表达
证明
(中括号里面的都是下标)
对于一个串T[0~j] ,串T和串S比对到S[i]的时候 前提引入
假设存在一个k,满足
T[0 ~ k-1] == T[j-k ~ j-1] (1)
当T[i] != S[j]时(发生不同,比如上面例子中 比对到字符6时发生不同),有
T[0 ~ j-1] == S[i-j+1 ~ i-1] (2)
(2)两边左界限同时+(j-k),得
T[j-k ~ j-1] == S[i-k+1 ~ i-1] (3)
由(1),(3) 得T[0 ~ k-1] == S[i-k+1 ~ i-1]
即只要k存在,就有 T[0 ~ k-1] == S[i-k+1 ~ i-1]
所以回溯到k
满足T[0 ~ k-1] == S[i-k+1 ~ i-1] 重点!!!!
所以T[0 ~ k-1]不用比较,它和S[i-k+1 ~ i-1]相等
回溯到k开始比较即可,这个证明是自洽的。
举几个例子提供尝试:
(记第一个位置为0,第二个位置为1,以此类推)
S:aabaabaaabaac
T:aabaabaac k = 5 (aabaa相同)
|
S:abababababac
T:abac k = 1 (a相同)
|
S:bbabbabbabbc
T:bbabbc k = 2 (bb相同)
|
S:abaaabaaabaac
T:abaaabaac k = 4 (abaa相同)
|
S:abaabaabaabaaabaac
T:abaabaac k = 4 (abaa相同)
|
...
这样一来,我们发现 j 的回溯位置只和子串本身有关,我们把要解析的子串拆分成更小的子串,每个子串都有它的k值,并把它保存在一个数组中,数组起个名字叫next,比如abaabcac这个串,我们将它逐步分解:
T: abaabcac k = 1 (a)
T: abaabca k = 0 (无相同,回溯0)
T: abaabc k = 2 (ab)
T: abaab k = 1 (a)
T: abaa k = 1 (a)
T: aba k = 0 (无相同,回溯0 )
T: ab k = 0 (无相同,回溯0 )
T: a k = -1 (第一个比较,没进入,没回溯,我们自己定个-1)
所以next[] = {-1,0,0,1,1,2,0,1}
k指向相应的回溯指针位置,即
每个字符 a,b,a,a,b,c,a,c
都有下标 0,1,2,3,4,5,6,7
比如我们比较到第一个c的时候发现不等,
第一个c的k值是next[5],next[5]保存的数值为2,j就回溯到下标2,也就是a
abaabaabcac
abaabcac
|
abaabaabcac
abaabcac
|
从上面我们可以看到,不同的子串 发生回溯的位置 k 有可能不同,数组next,保存相应的k值,也就是当比较到子串某一个字符,这个字符通过比较发现不相等时, j 要回溯的位置。
核心2 next数组的求解
我们知道,j 的回溯位置只和子串本身有关,下面我们给定一个子串T[1 ~ m](为解释方便从1开始)
T1...............................................Tm
假设子串某一个位置 j ,我们的目的是求它 j+1 的 k’值 (为了区别k起名k’)
(为什么目的是求j+1的k值?因为我们知道j=0的时候k值为-1,j=1时可以看前一项是否相等来决定,等下面知道情况1和情况2后再回来看,就知道这个目的和设k值一样,也是自洽的。)
T1.........................................Tj...Tm
j 位置的 k值存在,next[j] = k。
T1............T[k-1],Tk
>|< T[j] 的 k值指向这,next[j]
原则
则T[1 ~ k-1] == T[j-k+1 ~ j-1](不知道为什么的可以翻一下核心1的证明)
T1..................T[j-k+1]................T[j-1],Tj...Tm
T1......................T[k-1],Tk
|--------这里面都相等---------| >|< T[j]的k值指向这里,next[j]
要求T[j+1]的k’ 值,有两种情况
情况1、Tk == Tj
按照原则,T[j+1] 的 k’ 值 应该是 最大相等子串 中 前面子串 的后面。
因为Tk==Tj,所以包括Tk在内也是相等子串。
所以T[j+1]的k’值应该指向Tk的后面,也就是T[k+1]。
因为k+1指向T[k+1],所以T[j+1] 的k’值是k+1。
所以next[j+1] = k+1。
T1..................T[j-k+1]................T[j-1],Tj,T[j+1]...Tm
T1......................T[k-1],Tk,T[k+1]
|-----------这里面都相等----------| >|< T[j+1] 的k' 指向这里,这里是k+1的指向,也就是说next[j+1] = k+1
代码如下:
void Getnext(int next[],har T[])
{
int j=0,k=-1;
next[0]=-1;
while(j<T.length-1)
{
if(k == -1 || T[j] == T[k])
{
j++;k++;
next[j] = k;
}
else k = next[k];
}
}
情况一解释了 if 的执行部分
接下来的情况二,解释的就是判断部分中的if(k == -1)
和 else k = next[k]这段代码。
情况2、Tk != Tj
当Tk != Tj的时候,我们不能直接求T[j+1]的k’值。
由第一种情况我们可以知道,当Tk == Tj 的时候,后一项的k值是可以由前一项得出来的。
换句话说,当前项的 k 可以由上一项的 k 决定,当且仅当Tk == Tj 的时候。
现在的情况是Tk != Tj ,我们要找到 Tk 等于 Tj 的情况才可以递推求T[j+1]。
现在问题是,这个Tk到底在哪里才和Tj相等呢?明显当前的Tk不行,那就要重新找一个符合条件的Tk,使得Tk == Tj ,从而求得 next[j+1] = k+1。
为容易区分,我们把接着查找的Tk命名为T,它的k值命名为k’’,T如果符合T == T[j],就可以回到情况1。
现在我们理一下思路
1、我们的目的是求next[j+1]。
2、next[j+1] = k+1,而 next[j+1] = k+1 的前提是 Tk == Tj。
3、当前的Tk不符合 Tk == Tj。
4、我们往前找一个使它相等的Tk,记这个Tk为T。
5、问题转换为找T的位置。
首先,T要符合原则(不记得可以翻一下前面),如下
T1..................T[j-k''+1]................T[j-1] ,Tj,...Tm
T1........................T[k''-1],T
|-----------这里面都相等----------| >|< Tj的k''值指向这里
对比一下原本的Tk,一模一样,这是不行的
T1..................T[j-k+1]................T[j-1],Tj...Tm
T1......................T[k-1],Tk
|--------这里面都相等---------| >|< T[j]的k值指向这里
因为我们已经知道Tk != Tj,T肯定要换个位置找,不然就失去了意义。另一方面,T[k’’]的位置至少应该是在Tk的前面,因为我们本来就是由前项推后项,不可能从后面推前面,所以T的位置应该是:
T1..........T[k''-1],T.........Tk
把它放在有Tk的位置,是这样子的:
Tk的位置:
|—————————————————————| 相等 |—————————————————————————|
T1...............T[k-1],Tk..........T[j-k+1].............T[j-1],T[j]
放进去之后:
|————————————————————————————————| 相等 |———————————————————————————————————|
T1........T[k''-1],T.......T[k-1],Tk..........T[j-k+1].......T[j-k''+1]......T[j-1],T[j]
|————————————————| 相等 |——————————————————|
T1…T[k-1] 相等 T[j-k+1]…T[j-1],不妨放到上面,易于比较:
T1.............................T[k-1],Tk
|———————————————————————————————————|
T1........T[k''-1],T.......T[k-1],Tk..........T[j-k+1].......T[j-k''+1]......T[j-1],T[j]
|————————————————| 相等 |——————————————————|
T1…T[k’’-1] 相等 T[j-k’’+1]…T[j-1],不妨拉到下面,易于比较:
T1.............................T[k-1],Tk
|———————————————————————————————————|
T1........T[k''-1],T.......T[k-1],Tk..........T[j-k+1].......T[j-k''+1]......T[j-1],T[j]
|——————————————————|
T1...........T[k''-1],T
T[j-k’’+1]放进上面,单独拎出来:
T1...............T[j-k''+1]...............T[k-1], Tk
和下面对比:
T1...............T[j-k''+1]...............T[k-1], Tk
T1.......................T[k''-1], T
|——————————————————————————|
看一下原则,可以知道 T 的位置就是Tk的 k 值,也就是next[k]:
原则:
T1..............T[j-k+1]................T[j-1], Tj...Tm
T1......................T[k-1], Tk
|--------这里面都相等---------| >|< Tj的k值指向这里,next[j] <----注意Tj
我们得出的:
T1...............T[j-k''+1]...............T[k-1], Tk
T1.......................T[k''-1], T
|--------这里面都相等---------| >|< Tk 的k值指向这里,next[k] <----注意是Tk的k值
>|<这个位置是T[j]的k''值,k''指向这里,前面说过
所以 k’’ == next[k],知道了这个我们把 k’’ 赋值给 k,即k = next[k] ,让它带着 k’’ 进行下一轮的查找。
为什么要进行下一轮的查找?因为我们不知道这一轮查找到了没有。
如果T就是我们要找的T == Tj,就会回到情况1,给next[j+1]赋值了。
如果T还是不等于Tj,我们还是没有找到,要重新设一个Tk,起名Tx,k值命名为k’’’(这是为了解释的时候不混淆而设的,实际上不需要,k = next[k]就是下一轮查找)… …如此深入下去,有点像一个递归调用。
既然像递归调用,那肯定要有停下来的条件,很简单,如果k == -1(核心1的时候我们把第一个比较,没进入没回溯的k值设为-1)的话,说明前面找不到有Sa==Sb的,也就是说没有k值,那 j 就只能回溯到第一个位置,next[j+1]自然指向第一个字符的位置,k+1=-1+1=0。
代码很简洁,但是里面包含的逻辑证明很多,有紧密的自洽性,要真正理解起来还是需要多看几遍。