KMP算法的完全剖析(重点next数组)——使用证明方法理解KMP算法

核心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。

代码很简洁,但是里面包含的逻辑证明很多,有紧密的自洽性,要真正理解起来还是需要多看几遍。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值