【数据结构与算法】KMP算法

引言

字符串查找的朴素算法中,我们每次对目标字符串的查找失败后,目标字符串的指针回到 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算法

28. 找出字符串中第一个匹配项的下标

给你两个字符串 haystackneedle ,请你在 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 题目:

459. 重复的子字符串

给定一个非空的字符串 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;
}

你可能感兴趣的:(算法)