kmp算法是用于字符串匹配的快速算法。
假设我们要匹配的字符串为target,而待匹配的字符串为source。kmp的目标就是在小于O(n^2)的时间复杂度下完成字符串target在source的首次完全匹配。
假设如下情景:
| 字符串 | |
|---|---|
| source | bababcde |
| target | babc |
当我们直到了target的长度后,很容易想到,用滑动窗口法,逐步地取出source中与target等长的字符子串,若不匹配,则窗口不断右滑。
但是上面的方法正如前面第一段所说,时间复杂度太高,而且在匹配过程中我们只用了target的长度这一个信息,而没有关注target字符串本身的特点。
仔细观察target,我们发现babc中出现了两个b,如果我们在匹配的过程中,bab这前三个字符已经匹配成功了,而第四个字符匹配失败,这个时候滑动窗口会怎么做?
答:滑动窗口的指针从原来指向b,变为指向source的下一个字符,然后继续从target的第一个开始重新匹配。
这样做的效率无疑是很低的!
实际上我们目前已知bab是已经匹配成功的,这说明source串中已经找到bab串,那么说明第二个b后面一个字符是匹配失败的,但是前面的字符都匹配成功了,如果target中存在重复的前缀,由于前面匹配成功了,因此只要以当前的元素为上一个重复前缀的位置,然后继续向后匹配则实现了对滑动窗口的剪枝。
例如,b在原target中的上一个前缀是b,因此我们直接从b开始匹配就可以了,而滑动窗口会从a开始匹配,KMP直接跨过了a为首元素匹配的情况
如何来找这个上一个重复元素的前缀位置呢?
我们首先来用一个指针i来表示当前正在遍历的target元素。再定义一个指针j,用来表示当前元素在上一次出现的前缀位置。
初始时刻,j=0,而i = 1 如果,target[j]= target[i],则匹配成功,当前的i和j可以作为前缀,i和j都向前移。 如果,target[j]!=target[i],则匹配不成功,让j回退到他出现的上一个前缀位置,继续判断与target[i]的相等情况,直到相等或者j=0(j=0则说明不存在前缀,从0开始重新匹配)。
我们用pi数组来存储每个位置元素的上一个前缀位置。
以上面的target babc为例
初始时刻:j = 0;i= 1; target[j]!=target[i],j=0,则pi[i] = j; 说明a不匹配的话,下一次得重新从0开始匹配
接着i+1后,i = 2。 此时,target[j]= target[i],j向后移动继续匹配, pi[i] = j = 1。说明如果b的下一个元素,不匹配成功的话,可以将当前的b当作是target[0]位置的b来考虑,然后从是target[0]位置的b的下一个位置,也就是第1个位置进行匹配。
当i = 3时,target[i]=c,此时target[j=1]不等于c,然后继续返回1位置的上一个前缀,是0,则位置3的c不成功匹配的话,也会直接从0位置开始重新匹配target字符串。
所以 pi = [0,0,1,0];
下面是编程实现(思路就是寻找当前下标对应元素的在target中从头开始计数的上一个前缀位置):
public int strStr(char[] haystack,char[] needle){
int n = needle.length;
int[] pi = new int[n]
//构建前缀位置数组pi
for(int i = 1,j = 0;i<n;i++){
while(j>0&&needle[i]!=needle[j]){
j = pi[j-1];
}
if(needle[i]==needle[j]){
j++;//j是匹配的字符,继续匹配需要从匹配字符的下一个字符开始算是否匹配
}
pi[i]=j;
}
for(int i = 0,j = 0;i<haystack.length;i++){
while(j>0&&haystack[i]!=needle[j]){
j = pi[j-1];
}
if(haystack[i]==needle[j]){
j++;//j是匹配的字符,继续匹配需要从匹配字符的下一个字符开始算是否匹配
}
if(j==n){
return i-n+1;//返回匹配位置;
}
}
return -1;
}