背景
KMP算法看了很多回,但是每次回忆起来都有一些关键点感觉理解的很模糊,这次仔细研究了一下KMP,给出一个自己的理解。
构造next数组
1 构造数组,使得该数组满足i>0时, [ P 0 , P next i ) = [ P i − next i , P i ) [P_0, P_{\text{next}_i})=[P_{i-\text{next}_i},P_i) [P0,Pnexti)=[Pi−nexti,Pi),我们称其为一个不完全next数组,构造方法如下:
因为对任意nexti(i>0)有,故一个全零数组为不完全next数组。
- i=0的情况另行讨论
- [ P 0 P_0 P0, P next i P_{\text{next}_i} Pnexti)表示P的子串,前闭后开区间
现在我们来优化next数组,使数组内元素尽可能大。
若 P next i + 1 = P i + 1 P_{\text{next}_i + 1} = P_{i + 1} Pnexti+1=Pi+1, 对于 P next i P_{\text{next}_i} Pnexti有 [ P 0 , P next i ) = [ P i − next i , P i ) [P_0, P_{\text{next}_i})=[P_{i-\text{next}_i},P_i) [P0,Pnexti)=[Pi−nexti,Pi),
则 [ P 0 , P next i + 1 ) = [ P i − next i , P i + 1 ) [P_0, P_{\text{next}_i + 1})=[P_{i-\text{next}_i},P_{i + 1}) [P0,Pnexti+1)=[Pi−nexti,Pi+1)
设 j = i + 1 j=i+1 j=i+1, k= next i + 1 \text{next}_i+1 nexti+1,代入得 [ P 0 , P k ) = [ P j − k , P j ) [P_0, P_k)=[P_{j-k},P_j) [P0,Pk)=[Pj−k,Pj) 即 next j = k \text{next}_j=k nextj=k, 元素代回后有 next i + 1 = next i + 1 \text{next}_{i+1}=\text{next}_i+1 nexti+1=nexti+1
所以next数组的求法为,初始化一个全零数组,从1开始遍历,若 P next i + 1 = P i + 1 P_{\text{next}_i + 1} = P_{i + 1} Pnexti+1=Pi+1,则 next i + 1 = next i + 1 \text{next}_{i+1}=\text{next}_i+1 nexti+1=nexti+1;
private static int[] getNext(String pattern){
int[] next = new int[pattern.length()];
for (int i = 2; i < pattern.length(); i++) {
if(pattern.charAt(next[i-1]) == pattern.charAt(i-1)){
next[i] = next[i-1]+1;
}
}
next[0]=-1;
return next;
}
可以用反证法证明这样求出的数组是最大的不完全next数组,因为这个数组是最大的,所以对于任意 next i < k < i \text{next}_i<k<i nexti<k<i, 有 [ P 0 , P k ) ≠ [ P i − k , P i ) [P_0,P_k)\ne [P_{i-k},P_i) [P0,Pk)=[Pi−k,Pi) ,我们称这个最大的不完全next数组为next数组
证明KMP
设指针j、i分别指向主串和模式串中正在匹配的字符,则 S j − i = P 0 S_{j-i}=P_0 Sj−i=P0 是第一个匹配的字符,匹配过程中需要一直满足 [ S j − i , S j ) = [ P 0 , P i ) [S_{j-i},S_j)=[P_0,P_i) [Sj−i,Sj)=[P0,Pi) , 若
- 若 S j = P i S_j=P_i Sj=Pi ,易得 [ S j − i , S j + 1 ) = [ P 0 , P i + 1 ) [S_{j-i},S_{j+1})=[P_0,P_{i+1}) [Sj−i,Sj+1)=[P0,Pi+1) 。
- S j ≠ P i S_j\ne P_i Sj=Pi, 则令 j = j − i + 1 , i = 0 j=j-i+1,i=0 j=j−i+1,i=0 ; 有 [ S j − i + 1 , S j − i + 1 = 0 ) = [ P 0 , P 0 ) [S_{j-i+1},S_{j-i+1=0})=[P_0,P_0) [Sj−i+1,Sj−i+1=0)=[P0,P0) ,遍历 S j − i + 1 = P 0 S_{j-i+1}=P_0 Sj−i+1=P0 是第1个匹配的字符的情况
此时为暴力匹配算法
public static int bfFind(String S, String P) {
char[] arr1 = S.toCharArray();
char[] arr2 = P.toCharArray();
int i = 0;
int j = 0;
while(j < arr1.length && i < arr2.length) {
// kmp这里要补上arr1[j] != arr2[0]的情况
// if(i==0 && arr1[j] == arr2[i]){j=j+1;continue;}
if(arr1[j] == arr2[i]) {
j++;
i++;
}
else {
j = j - i + 1;
i = 0;
// kmp, 这里替换成 j=j; i=next[i];
}
}
if(i == arr2.length) return j - i;
else return -1;
}
如果 S j + i ≠ P i S_{j+i} \ne P_{i} Sj+i=Pi, 因为 [ S j − i , S j ) = [ P 0 , P i ) [S_{j-i},S_j)=[P_0,P_{i}) [Sj−i,Sj)=[P0,Pi),所以 [ S j − next i , S j ) = [ P i − next i , P i ) [S_{j-\text{next}_i},S_j)=[P_{i-\text{next}_i},P_i) [Sj−nexti,Sj)=[Pi−nexti,Pi),又根据next数组的定义有 [ P i − next i , P i ) = [ P 0 , P next i ) [P_{i-\text{next}_i},P_i)=[P_0,P_{\text{next}_i}) [Pi−nexti,Pi)=[P0,Pnexti),所以 [ S j − next i , S j ) = [ P 0 , P next i ) [S_{j-\text{next}_i},S_j)=[P_0,P_{\text{next}_i}) [Sj−nexti,Sj)=[P0,Pnexti)
又因为 对于任意$\text{next}_i<k<i, 有 [ P 0 , P k ) ≠ [ P i − k , P i ) [P_0,P_k)\ne [P_{i-k},P_i) [P0,Pk)=[Pi−k,Pi),即 [ S j − k , S j ) ≠ [ P 0 , P k ) [S_{j-k},S_j)\ne[P_0,P_k) [Sj−k,Sj)=[P0,Pk) ,所以可以跳过第一个匹配字符在 [ S j − i , S j − next i ) [S_{j-i}, S_{j-\text{next}_i}) [Sj−i,Sj−nexti) 中的情况
next[0] = -1 的情况
S j ≠ P 0 S_j\ne P_0 Sj=P0时,有j=j+1, i=i+1=0,遍历下一种情况,这样就不用单独处理 S j ≠ P 0 S_j\ne P_0 Sj=P0的情况了,贴一下优化后的代码。
private static int[] getNext(String pattern) {
//获取两串的字符数组,以便遍历
char patternOfChars[] = pattern.toCharArray();
//创建next数组
int[] next = new int[pattern.length()];
int nextValue = -1, loopOfPattern = 0;//初始化next值及模式串下标
next[0] = -1;//这里采用-1做标识
while(loopOfPattern < pattern.length() -1)
{
//获取next数组
if(nextValue == -1 || patternOfChars[loopOfPattern] == patternOfChars[nextValue])
{
nextValue++;
loopOfPattern++;
next[loopOfPattern] = nextValue;
} else {
nextValue = next[nextValue];
}
}
return next;
}
public static int kmp(String str, String pattern) {
//如果主串长度不小于模式串,则进入模式匹配
if (str.length() >= pattern.length()) {
//获取next数组
int next[] = getNext(pattern);
//获取两串的字符数组,以便遍历
char strOfChars[] = str.toCharArray();
char patternOfChars[] = pattern.toCharArray();
//两个循环控制变量
int loopOfStr, loopOfPattern;
//遍历主串,任意一串遍历结束,则匹配结束
for (loopOfStr = 0, loopOfPattern = 0 ; loopOfStr < str.length() && loopOfPattern < pattern.length() ;) {
//如果两元素相同,或模式串全部匹配失败,比较下一个元素
if (loopOfPattern == -1 || strOfChars[loopOfStr] == patternOfChars[loopOfPattern]) {
loopOfStr++;
loopOfPattern++;
} else {
loopOfPattern = next[loopOfPattern];//模式串下标置为next值
}
}
//模式串匹配结束,表示匹配成功
if (loopOfPattern == pattern.length()) {
return loopOfStr - loopOfPattern;//主串中模式串第一次出现的位置
}
}
//模式匹配失败
return -1;
}