KMP算法
背景
在刷leetcode时,遇到了这样一个问题,给定两个字符串A和B,问你在A中是否有字符串B,并给出A中B出现的起始位置。
拿到这道题,首先想到的是使用暴力搜索的方法,直接在A中去搜索B,这种方法太暴力,当出现不匹配字符时,遍历A和B的指针需要同时回退,时间复杂度高,为O(m*n),其中m,n分别是字符串A和B的长度。而且,当字符串中重复的元素比较多时,这种解法会有很多不必要的操作。
暴力搜索的代码
int search(string A, string B) {
if(B.empty())
return 0;
int i = 0,j = 0;
int sizeA = A.size();
int sizeB = B.size();
while(i < sizeA && j < sizeB)
{
if(A[i] == B[j])
{
i++;
j++;
}
else //只要遇到不匹配字符,两个指针都要回退
{
i = i - j + 1;
j = 0;
}
}
if(j == sizeB)
return i - j;
return -1;
}
我们发现这样一位一位的匹配很浪费,没有利用到已经进行匹配的有用的信息。
KMP算法的不同之处在于它会花费一部分空间来记录一些信息。
介绍
KMP算法是一个改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,KMP算法的核心是尽量使用匹配失败后的信息,尽量减少模式串与主串的匹配次数来达到快速匹配的目的。
两个重要的概念
前缀:
一个字符串的前缀是指除最后一个字符以外,这个字符串的全部头部字符组成;
例如,字符串banana的前缀有b,ba,ban,bana,banan
后缀:
同理,一个字符串的后缀指除第一个字符以外,这个字符串的全部尾部字符组成;
例如,字符串banana的后缀有a,na,ana,nana,anana
next数组的解法
KMP算法中一个重要的步骤就是对模式串构建next数组,next数组的长度为模式串的长度,
(这里约定数组的下标i表示对模式串中前i个字符构成的子串进行计算的结果值),这个结果值是子串的相同前缀和后缀的最长长度。
例如,模式串为abcdabd,当字串为abcda时,其前缀组合为a,ab,abc,abcd,后缀组合为a,da,cda,bcda,相同的前缀后缀为a,其长度为1。
这里,演示一下,计算next前几个值的步骤(模式串为abcdabd):
当i = 0时,子串为空,为无前缀和后缀,next[0] = -1;
当i = 1时,子串为a,前缀和后缀均为空集,next[1] = 0;
当i = 2时,子串为ab,前缀为a,后缀为b,没有交集,next[2] = 0;
.
.
.
当i = 6时,子串为abcdab,前缀为a,ab,abc,abcd,abcda,后缀为b,ab,dab,cdab,bcdab,组合的交集为ab,长度为2,故next[6] = 2;
因此,next= {-1,0,0,0,0,1,2}
next数组的运用
这里的next数组就是事先对模式串计算的内部匹配信息,这个匹配信息可以在字符匹配失败时,对模式串进行最大的移动,以减少如暴力匹配中的那么多的匹配次数。
比如,在一次匹配失败后,我们会想将模式串尽量的右移和主串进行匹配,对模式串右移的距离计算方式为,在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,然后移动使他们重叠。
例如:
主串A为 abcdababcdabde
模式串B为 abcdabd
匹配的过程如下图:
KMP算法每次都是在匹配失败的地方,根据已经事先计算好的内部匹配信息,来移动模式串,使匹配过程从匹配失败的地方继续进行匹配,从而减少了匹配的次数。
算法时间复杂度为O(m+n),事先计算内部匹配信息需要花费O(n),字符串匹配时需要花费O(min(m,n));
空间复杂度为O(n),需要一部分空间来存储对模式串计算的内部匹配信息。
实现代码如下:
vector<int> getNext(string str)
{
vector<int> next;
int len = str.size();
next.push_back(-1);//next数组的初始值设置为-1,第一个元素没有前缀
int i = 0;
int k = -1;
while (i < len -1)
{
if (k == -1 || str[i] == str[k])//str[i]表示后缀,str[k]表示前缀
{
k++;
i++;
next.push_back(k);
}
else
{
k = next[k];
}
}
return next;
}
int search(string haystack, string needle) {
//方法2,使用KMP算法
if (needle.empty())
{
return 0;
}
int size1 = haystack.size();
int size2 = needle.size();
int i = 0;
int j = 0;
vector<int> next;
next = getNext(needle);
while (i < size1 && j < size2)
{
if (j == -1 || haystack[i] == needle[j])
{
i++;
j++;
}
else
{
j = next[j];//当当前字符不匹配时,匹配字符串需要重新定位下一个匹配字符的位置
}
}
if (j == size2)//若匹配循环退出且匹配的字符串达到末尾,此时匹配成功
{
return i - j;
}
return -1;//表明未匹配成功
}
另外可以看另一篇将KMP算法的文章,容易看懂