KMP算法

KMP算法简介:

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。

找子串同样也有逻辑思路简单的的BF算法 ,也就是暴力解法,如下图所示:
我们有主串ABCABCDHIJK 和子串 ABCE
BF算法是先从两串的第一个元素开始判断元素是否相等:
请添加图片描述

如果元素相等的话就接着判断两串的下一个元素,如果遇见不相等的情况的时候如下图:
请添加图片描述
遇到上图的情况时,就让i回溯到最初位置的下一个位置此处为下标为1的位置,让j回溯到下标为0的位置,然后又一个一个地判断元素是否相同,如下图:
请添加图片描述

上面就是BF算法的大体逻辑,十分简单,但是我们很明显可以发现这样的算法时间复杂度有可能会很高,假设主串长度为n,子串长度为m,最高时间复杂度为O(m*n),最低为O(m),但是可想而之一般情况下的时间复杂度都会比较高的。

KMP算法就是为了解决这一问题的,KMP算法的主要优点就是省去了主串的回溯过程,增加一点空间复杂度(空间复杂度为O(m))来大幅降低时间复杂度,逻辑如下图:
首先我们先从两串的第一个元素开始逐个判断两元素是否相同,遇到两元素不同的情况如下图:
请添加图片描述
遇到上图的情况时,我们进行的操作是判断子串j下标前的字符串中的最长相等前后缀,此处的最长相等前后缀为AB,至于最长相等前后缀的概念我们下面马上讲解,找到最长相等前后缀后,我们的操作是让主串的i不变,子串的j指向最长相等前缀的下一个元素,然后继续与主串的i下标的元素继续开始逐个比较,如下图:请添加图片描述
我们发现,这样就可以直接跳过多次无用的比较,直接有用的地方开始比较。

最长相等前后缀是什么:

我们以字符串abccab为例:
前缀的集合为:{a, ab, abc, abcc, abcca}
后缀的集合为:{b, ab, cab, ccab, bccab}
从上面两集合中我们可以清晰看到最长相等前后缀为ab
例如:cacac的最长相等前后缀为cac
然而,也有最长相同前后缀不存在的情况,如:abcbc

了解了什么是最长相等前后缀,我们就可以接着了解KMP算法的灵魂next数组了,由于一个字符串中的每一个字符前的字符串都有可能有最长前后缀,而且最长相等前后缀的长度是我们移位子串去匹配主串的关键,所以我们单独用一个next数组存储子串的每个元素前的字符串的最长相等前后缀的长度。
例如子串abcabc的next数组如下表所示:

abcabc
next[0]next[1]next[2]next[3]next[4]next[5]
-100012

其中next[0]为-1的原因是下标为0的a的前面没有元素可处理(没有前缀),其余next[]为0的情况是前缀中没有最长相等前后缀或最初长相等前后缀只是一个字符,为0的话就可以当与主串元素不符的时候j直接指向子串的第一个元素继续与主串比较。
总结next数组的作用:

  1. next[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度。
  2. 表示该处字符不匹配时应该回溯到的字符的下标

next数组的代码如下:

代码中s为主串,t为子串

//串的数据结构如下:
typedef struct {	
	char data[MaxSize];
	int length;//串长
} string;

//由模式串t求出next值
void GetNext(string t, int next[]) {
	int j, k;
	j = 0;
	k = -1;
	next[0] = -1;//第一个字符前无字符串,给值-1
	//因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
	//所以最后一次经过while循环时j为t.length-2
	while (j < t.length - 1) {	
		//k为-1或比较的字符相等时
		if (k == -1 || t.data[j] == t.data[k]) {	
			//对应字符匹配情况下,s与t指向同步后移
			j++;
			k++;
			next[j] = k;
       	} else {
       		//对k进行回溯
			k = next[k];
		}
	}
}

其中,k在执行回溯前t.data[j]已经有了一对最长相等前后缀,然而,该最长前缀字符串中也有自身的最长相等前后缀,且与t.data[j]前的最长相等后缀自身的最长相等前后缀相同,而k的回溯实际上就是将k指向t.data[j]前的最长相等前缀自身的最长相等前缀的下一个元素。
详见此位大佬的博客:大佬博客
其中大佬博客中的图片讲解如下:
在这里插入图片描述
在这里插入图片描述
KMP完整代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct {
    char data[100];
    int length;//串长
} string;

//由模式串t求出next值
void GetNext(string t, int next[]) {
    int j, k;
    j = 0;
    k = -1;
    next[0] = -1;//第一个字符前无字符串,给值-1
    //因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
    //所以最后一次经过while循环时j为t.length-2
    while (j < t.length - 1) {
        //k为-1或比较的字符相等时
        if (k == -1 || t.data[j] == t.data[k]) {
            //对应字符匹配情况下,s与t指向同步后移
            j++;
            k++;
            next[j] = k;
           } else {
               //对k进行回溯
            k = next[k];
        }
    }
}

//KMP算法
int KMPIndex(string s, string t) {

    int next[100], i = 0, j = 0;
    GetNext(t, next);
    while (i < s.length && j < t.length) {
        if (j == -1 || s.data[i] == t.data[j]) {
            //i,j各加1
            i++;
            j++;
        }
        else {
            //i不变,j后退,即重新让子串的相应元素匹配主串i位置的元素
            j = next[j];
        }
    }
    //如果j能成功走到子串的最后一个元素就证明成功匹配
    if (j >= t.length) {
        return(i-t.length);//返回匹配模式串的首字符下标
    } else {
        return -1; //返回1表示主串和子串不匹配
    }
}

//主函数
int main (void) {
    int x;
    string s, t;
    s = *(string *)malloc(sizeof(string));
    t = *(string *)malloc(sizeof(string));
    printf("请输入相应的主串和子串:\n");
    scanf("%s%s", s.data, t.data);
    s.length = (int)strlen(s.data);
    t.length = (int)strlen(t.data);
    x = KMPIndex(s, t);
    if (x == -1) {
        printf("error");
    } else {
        printf("子串第一个下标位置为:%d\n", x);
    }
}

运行结果示例:
请添加图片描述
请添加图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值