KMP算法
**摘要:本文将介绍KMP算法的基本原理和实现方式,并提供C语言示例代码,帮助读者理解该算法的工作原理和如何在实际应用中实现。
要在文本字符串里面找到目标字符串,我们很容易就能想到一个暴力的解法:就是从文本串里面一个一个和目标串比对。
假如说文本串是BBC ABCDAB ABCDABCDABDE
目标串是ABCDABD
但是要是遇到了不匹配的地方,指向文本串的指针就得回到一开始的位置
这个方法在最坏的情况下要将文本串全部遍历过去,目标串也会随之遍历n遍,效率极其不高!
可是细心观察的话就会发现,从文本串不匹配的A到空格,其后面都是不符合条件的子串,但是要怎样做能让下次遍历直接就从后面开始,避免这种情况呢?
这个时候就要提到本文的主角了:KMP算法。
介绍
KMP(Knuth-Morris-Pratt)算法是一种用于在一个文本串(字符串)中查找一个模式串(字符串)的高效算法。与朴素的字符串匹配算法相比,KMP算法具有更低的时间复杂度,特别是在处理大型文本和模式时。
KMP算法的核心思想是利用模式串自身的特点,在匹配过程中尽量减少重复的比较次数。具体来说,KMP算法通过预处理模式串,构建一个部分匹配表(在下文中我们叫next数组),从而在匹配过程中跳过一些不必要的比较。
KMP算法的关键就在于这个next数组,前面提到如果出现了像ABCDABD这样的目标串,匹配到后面的D时发现与文本串不相符,我们可以从将指向D的指针回退到C继续开始比对,这样就省去了将指向文本串的指针回退的操作。
仔细观察的话可以发现,目标串中起始有AB,后半部分也有AB,那这样算是什么特点呢?
这里就要提到前缀和后缀了:
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
这样将可能不太好理解,可以看下图:
就是说,最大公共元素长度其实记录着下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
有了这个表,回到我们文章最开始的地方
在这里目标串与文本串匹配失败,观察到目标串D(即匹配失败的位置)的前一位B所对应的最大前缀后缀公共元素长度是2,我们就可以知道D的前两位和目标串起始两位是一样的,所以这个时候就可以直接将指向目标串的指针回退到指向C就可以了。
然后再不匹配的话就是一样的原理,通过观察相应的最大前缀后缀公共元素长度就能知道将指针回退到哪个位置了。
由于要观察匹配错误的位置的前一位所对应的最大前缀后缀公共元素,通过将这个数组整体像右移一位,首位置为-1,就能得到我们的next数组了:
目标串 | A | B | C | D | A | B | D |
---|---|---|---|---|---|---|---|
最大长度值 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
next数组 | -1 | 0 | 0 | 0 | 0 | 1 | 2 |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
所以有:失配时,目标串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即指针指向next值的下标位置。
int strStr(char* haystack, char* needle) {
int i=0;
int j=0;
int sLen=strlen(haystack);
int pLen=strlen(needle);
int* next = malloc(sizeof(int) * pLen);
GetNext(needle,next);
while(i<sLen&&j<pLen)
{
if(j==-1||haystack[i]==needle[j])
{
i++;
j++;
}
else
{
j=next[j];
}
}
if(j==pLen)
return i-j;
else
return -1;
}
这个就是最后的函数实现。
当然还有最关键的求next数组的函数
void GetNext(char* p,int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
if (k == -1 || p[j] == p[k])
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k];
}
}
}
f (k == -1 || p[j] == p[k])
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k];
}
}
}