学习了KMP算法,对此有了一些理解,通过博客分享,如有理解错误的地方,请纠正!
字符串的前缀后缀
再说明KMP算法前见说下它用到的一些东西。给定一个字符串如 “ABCDAB”,那么它的前缀就是除去最后一个字符的所有串即“ABCDA,ABCD, ABC,AB,A”,同理,后缀是出去第一个字符的串即“BCDAB,CDAB,DAB,AB,A”,而在KMP算法中我们要用到要匹配串的子串的前缀后缀的最大公共长度。我们还用“ABCDAB”这个串举例。
各个子串 | 前缀 | 后缀 | 最大公共长度 |
---|---|---|---|
A | 空 | 空 | 0 |
AB | A | B | 0 |
ABC | AB,A | BC,B | 0 |
ABCD | ABC,AB,A | BCD,CD,D | 0 |
ABCDA | ABCD,ABC,AB,A | BCDA,CDA,DA,A | 1 |
ABCDAB | ABCDA,ABCD,ABC,AB,A | BCDAB,CDAB,DAB,AB,A | 2 |
所以你可以用一个数组来记录出各个子串的前缀后缀最大公共长度
A | B | C | D | A | B |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 2 |
最大公共长度数组获取
这里面的思想有些类似动态规划,我们用图来说明下。这里把数组命名为arr,字符数组为str
我们知道字符串的第一个字符的子串没有前后缀,公共长度一定为0
所以我们从后面开始 J表示后缀与前缀相同的最大公共长度,有上面的表格可以看出来要想出现前后缀相同,肯定是前缀的头和后缀的尾相等。
所以这里判断str[j]与str[i]是否相等,结果不等,故子串“AB”没有前缀和后缀相同的的子串即
同理看子串“ABC”和子串“ABCD”中也是没有相同的前缀和后缀即
到了这里我们发现arr[j] == arr[i],即子串“ABCDA”中的前缀“A”和后缀的“B”相等,j++即
当前的最大长度是1,我们发现arr[j] == arr[i],所以很容易可以推出子串“ABCDAB”中的前缀“AB”和后缀“AB”相等,故现在的最大长度是 j++ = 2 ,同理“ABCDABC”和“ABCDABCD”子串也符合当前的情况即
到这里我们发现arr[j] != arr[i],故不能通过前面的最大长度去推该子串的最大长度,j = 4>0,所以我们要回溯到这一连续的前后缀相等的上一个状态,即让 j = arr[j-1],此时j = 0故正好在字符‘A’处,也正是判断子串“ABCDA”是否有前后缀相同的时候,只不过这是我们看的就是“ABCDABCDE”中前缀“A”和“E”是否相同了,结果不同。此时J = 0,没有前后缀相等的时候故“ABCDABCDE”这个子串也就没有相同的前后缀。即
附上代码
void getArr(char* str,int** arr){
int len = strlen(str);
*arr = (int *)malloc(sizeof(int) * len);
(*arr)[0] = 0;
for (int i = 1,j=0; i <len ; ++i) {
while (j>0 && str[j] != str[i])
j = (*arr)[j-1];
if (str[j] == str[i])
j++;
(*arr)[i] = j;
}
}
KMP算法
现在我们开始讲解KMP算法,它可以解决这样一个问题:有一个文本串S,和一个模式串P,查找P在S中的位置中第一次出现的位置。我们用图来说明这个算法。变量j表示匹配到相等元素的个数,str1为总串,str2为模式串
首先判断“C”与"A"不相等,子串向后面移动一位"B"与"A"也不相等子串继续往后移动寻找匹配即
直到找到相等这时j++,继续匹配下面直到遇到“D”!=“B”时
如果用暴力我们就会用把子串在整体向后移动一位,让模式串中的第一个字符“A”与总串的“B”进行匹配即
但是我们会发现这样要浪费很多时间,做无用的判断。KMP算法的解决方案是计算最大的移动位数,减少时间。我们看上面的图,要想再完全匹配到总串,只要找到后面总串中出现与模式串首字母一样的位置,才有可以总串之后的与模式串相匹配即
让子串移动到这里,这样是不是就移动了4位,比之前移动1位的效率高很多。那么移动的这四位是怎么计算出来的呢?
在模式串“B”时匹配失败,即我们可以查到“ABCDAB”这个串的前缀后缀公共元素最大长度是2,前面已经匹配成功5个字符了,我们用匹配成功的字符-失败的上字符的最大长度 即 5-1=4,即子串向后移动4位。可以和上面求arr类比 j=arr[j-1] 即j=1 即
此时先恰好是“B”与“D”匹配失败 j此时是1 1-0=1,即模式串向后移动一位此时J=0即
继续依次匹配,直到出现总串与模式串相等的时候即
之后"A" == “A”,“B” == “B” …… 此时 j=6=str2的长度,说明匹配成功,返回此时str1的数组下标即可。
int Search(char* str1,char* str2){
int* arr;
getArr(str2,&arr);
int len = strlen(str1);
for (int i = 0,j = 0; i <len ; ++i) {
while (j>0 && str1[i] != str2[j])
j = arr[j-1];
if (str1[i] == str2[j])
j++;
if (j == strlen(str2)){
free(arr);
arr = NULL;
return i-j+1;
}
}
free(arr);
arr = NULL;
return -1;
}
还有一种数组存储是在前后缀最大值公共长度数组的基础上演变的即next表即
很明显此时表是把最大值公共长度整体移动一位,前一位补-1,这样的好处则是在判断要移动模式串最大位数是直接用 完成匹配数-失败位的next中的值即可。
时间复杂度
我们分析下KMP算法的时间复杂度,生成数组,时间复杂度可估算为O(m),遍历主串可以估算为O(n),所以KMP算法的整体时间复杂度是O(m+n),m是模式串的长度,n是主串的长度。
我们在再分析下暴力,对主串遍历一番遇到匹配再和模式串匹配,不符合,从主串的下一位接着匹配。总体时间复杂度就是O(m*n)了!