字符串查找的朴素算法中,我们每次对目标字符串的查找失败后,目标字符串的指针回到 0 初始位置,这样的解法在处理大规模数据时往往不尽人意。为了避免朴素算法的低效,D.E.Knuth、J.H.MorTis 和 V.R.Pratt 联合发表了一个模式匹配算法即KMP算法——分别取三位学者名字的首字母得名,它可以一定程度上避免重复遍历的时间问题。
在了解KMP算法前,我们需要补充一个概念即字符串的前缀和后缀,前缀表示字符串不包含末尾字符的所有子串,后缀表示字符串不包含首字符的所有字串。如:
字符串 aabb 的前缀有:a,aa,aab ;后缀有:b,bb,abb。
KMP算法与前缀表(表示成数组名为 next 的数组)息息相关,它记录了目标串的每个字串前缀和后缀相等的最长子串的数目,对应相同子串前缀末尾,决定着查找串与目标串查找失败后目标串指针的回退位置。相当于在失败后目标串前序 next 数目和查找串后序相同数目进行匹配,只需要令指针从匹配子串的后一位置开始查找即可。如字符串 issis 的 前缀表为:
next [ 0 ] = 0 ,i 没有子串,故为 0 ;
next [ 1 ] = 0 ,is 的前后缀 i 和 s 不相同,故为 0 ;
next [ 2 ] = 0 ,iss 的前后缀 i is 和 s ss 不相同,故为 0;
next [ 3 ] = 1 ,issi 的前后缀 i is iss 和 i si ssi 存在 1 个相同的子串且长度为 1 ,故为 1;
next [ 4 ] = 2 ,issis 的前后缀 i is iss issi 和 s is sis ssis 存在 1 个相同的子串且长度为 2 ,故为 2;
在这里借助引入的一道 Leetcode 题目来实现KMP算法:
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
解法显然:
1,先找到 next 数组。
在这里定义了 gainNext 函数来解决:
int* gainNext(char* s, int nSize){
int* next=(int*)calloc(nSize,sizeof(int));//动态内存分配并初始化为0。
int front = 0;
for(int back = 1; back<nSize; back++){//前后缀末字符的理想差为1,因此后指针从下标1开始遍历。
while(front>0 && s[front]!=s[back]){
front = next[front-1];
}
if(s[front]==s[back]){
front++;
}
next[back] = front;
}
return next;
}
front 指向前缀末尾,back 指向后缀末尾。前后指针匹配失败时,前指针向 0 逐位移动尝试与后指针匹配,匹配成功则前后指针前移并将前指针赋给后指针下标的 next 元素。后指针只在开始每次循环时 ++ 一次,既是后缀末尾字符下标,也是 next 数组存储计数器。如果前后指针一直匹配失败直到前指针指向 0,那么跳出 while,next 的 back 下标值为 0。
2,根据 next 移动目标串指针跟随查找串指针遍历查找串。
int strStr(char* haystack, char* needle) {
int nSize = strlen(needle),hSize = strlen(haystack),slow = 0;
int* next = gainNext(needle,nSize);
for(int fast = 0; fast < hSize; fast++){
while(slow>0 && haystack[fast] != needle[slow]){//回退直到与当前快指针下标的字符相等。
slow = next[slow-1];
}
if(haystack[fast] == needle[slow]){
slow++;
}
if(slow == nSize){//slow在目标串末字符匹配成功后++,此时与目标串的长度相等。
free(next);
return fast-nSize+1;
}
}
free(next);//好习惯。
return -1;//跳出循环说明未找出,返回-1。
}
循环部分与 next 数组的建立较为类似,慢指针回退直到下标字符与快指针遍历查找的字符相等,慢指针 ++,再检测慢指针是否与目标串的长度相等,是则返回值结束;非则继续循环。
再次引入另一道 Leetcode 题目:
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
先给到一个例子 ababab ,它的前缀表 next 如下:
next [ 0 ] = 0,next [ 1 ] = 0,
next [ 2 ] = 1,next [ 3 ] = 2,
next [ 4 ] = 3,next [ 5 ] = 4;
前缀表 即 最长相等前后缀,若一个字符串 s 完全由 n 个最小重复子串 si 组成,那么其最长相等前后缀的长度是 n-1 * strlen ( si ) 。
非最长相等后缀子串的部分(长度为 strlen ( si ) )可以被 strlen ( s ) 整除。
所以我们求出 next [ strlen ( s ) - 1 ] 的值,利用它求得非最长相等子串,检测其是否能整除 strlen ( s ) 即可。
核心代码部分如下:
bool repeatedSubstringPattern(char* s) {//gainNext略
int* next=gainNext(s);
int sSize=strlen(s);
if(sSize % (sSize-next[sSize-1]) == 0 && next[sSize-1] != 0){
free(next);
return true;
}
free(next);
return false;
}