KMP算法的时间复杂度及优化

补充:先介绍下字符串前后缀。

字符串的前缀:符号串左部的任意子串(或者说是字符串的任意首部)
字符串的后缀:符号串右部的任意子串(或者说是字符串的任意尾部)

例:字符串:“abcdef”
该字符串前缀有:空串、“a”、“ab”、“abc”、“abcd”、“abcde”、“abcdef”
该字符串后缀有:空串、“f”、“ef”、“def”、“cdef”、“bcdef”、“abcdef”

一、暴力字符串匹配

在讲kmp之前,先来看一下传统的暴力字符串匹配,假设主串S的长度为M,子串T的长度为N,时间复杂度O(M * N)。如下for循环。
    for (int i = 0; i < strlen(S); i++) {
        for (int j = 0; j < strlen(S); j++) {
            if (S[i] == S[j]) {
                ......
            } else {
                ......
            }
        }
    }

可以看到,此处没有任何优化,已处理过的字符对未处理的字符没有任何指导意义,那是否有一种算法,可以让已处理过的字符对未处理的字符有指导意义。

二、KMP算法

kmp算法就不介绍了,我们先来看一下kmp核心思想:
1.查找字符串之前,先要对查找的字符串做一个分析,这样可以加速匹配过程。
2.匹配失败S串不用回溯。

例1:
S串:”abcdabce“
T串:”abce"
开始比较:
第一轮比较 S[0] = T[0]
第二轮比较 S[1] = T[1]
第三轮比较 S[2] = T[2]
第四轮比较 S[3] 不等于 T[3]了,这时怎么办呢,老办法从S[1],T[0]开始再比一次,这样效率很低,让我看看有哪些步骤可以省去:
已知:T[0] !=T[1] !=T[2] !=T[3],S[0] = T[0],S[1] = T[1],S[2] = T[2]
证明:S[1] != T[0],S[2] != T[0],
解:因为 S[1] = T[1],S[2] = T[2]
所以 S[1] != T[0],S[2] != T[0],等价于T[1] != T[0],T[2] != T[0]
又因为 T[0] !=T[1] !=T[2] !=T[3]
所以 T[1] != T[0],T[2] != T[0]成立,证明完毕。
综上:我们可以直接从,S[3]、T[0], 开始比较,省去一些没有意义的比较过程,所以得出如果我们先分析出了T串的一些特性(如T串各个字符不相对),那是可以加速一些匹配过程的。

是不是有了一点感觉呢,再让我们再来看第二个例子,你会更加有感觉:
例2:
S串:”aabaaaaaab“
T串:”aabaac"
开始比较:
第一轮比较 S[0] = T[0]
第二轮比较 S[1] = T[1]
第三轮比较 S[2] = T[2]
第四轮比较 S[3] = T[3]
第五轮比较 S[4] = T[4]
第六轮比较 S[5] 不等于 T[5]了,那下一轮我们要如何比较呢,那些过程可以省去,不急,让我们来分析一下已经比较过的字符串"aabaa",该串的前缀串"aa”,后缀串"aa"是一样的。
已知 T[3] = T[0],T[4] = T[1],S[0] = T[0], S[1] = T[1],S[3] = T[3],S[4] = T[4]
可求得:S[3]=T[0], S[4]=T[1]
综上 S[3]=T[0], S[4]=T[1],就不用比较了,直接求S[5] 是否 等于 T[2]就可以了。省去了一部分步骤。

看到这里有感觉了吗,对T串的预处理,可以加速我们的匹配过程。

在KMP算法中,对T串的预处理之后,会生成一个数组,我们称他为next数组,来加速匹配过程。

三、next数组的推导。

一句话概述:对i位置求next数组的值时,取出0 ~ i-1位置对应的字符串,求出该字符串最长且一样的前后缀长度(不包含自身)。
默认定义:
next[0] = -1;
next[1] = 0;
例1:
“aabaac”,对应的next数组为:
数组索引 0 1 2 3 4 5
字符串: a a b a a c
next数组: -1 0 1 0 1 2
具体分析:
i = 2时,0 ~ 1对应的字符串为aa,最长且一样的前缀和后缀都是a,所以值是1.
i = 3时,0 ~ 2对应的字符串为aab,不存在一样的前缀和后缀,所以值是0.
i = 4时,0 ~ 3对应的字符串为aaba,最长且一样的前缀和后缀都是a,所以值是1.
i = 5时,0 ~ 4对应的字符串为aabaa,最长且一样的前缀和后缀都是aa,,所以值是2.

例2:
“aaaaac”,对应的next数组为:
数组索引 0 1 2 3 4 5
字符串: a a a a a c
next数组: -1 0 1 2 3 4
具体分析:
i = 2时,0 ~ 1对应的字符串为a,最长且一样的前缀和后缀都是a,所以值是1.
i = 3时,0 ~ 2对应的字符串为aa,最长且一样的前缀和后缀都是aa,所以值是2.
i = 4时,0 ~ 3对应的字符串为aaa,最长且一样的前缀和后缀都是aaa,所以值是3.
i = 5时,0 ~ 4对应的字符串为aaaa,最长且一样的前缀和后缀都是aaaa,,所以值是4.

例3:
“abcdef”,对应的next数组为:
数组索引 0 1 2 3 4 5
字符串: a a a a a c
next数组: -1 0 0 0 0 0
具体分析:
i = 2时,0 ~ 1对应的字符串为a,不存在一样的前缀和后缀,所以值是0.
i = 3时,0 ~ 2对应的字符串为aa,不存在一样的前缀和后缀,所以值是0.
i = 4时,0 ~ 3对应的字符串为aaa,不存在一样的前缀和后缀,所以值是0.
i = 5时,0 ~ 4对应的字符串为aaaa,不存在一样的前缀和后缀,所以值是0.

注:若0~i位置的最长且相等的前后缀字符串为x,则0到i + 1位置的前后缀字符串最长长度不会超过x + 1。

next数组的原理是不是清楚了,先看下相关代码。

int *get_next(char *T)
{
    int len = strlen(T) + 1; // 防止越界,这里多申请一个空间
    int *next = malloc(sizeof(int) * len);
    memset(next, 0, sizeof(int) * len);
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int index = 0; /* 前缀字符串最后一个字符的索引 */

    while (i < len - 1) {
    	/* 判断前后缀字符串最后一个字符是否一致,若一致,则得到i位置最大前后缀字符串长度 */
    	/* 若不一致,index不断回跳,直致index等于0 */
        if (T[i - 1] == T[index]) {
            next[i++] = ++index;
        } else if (index > 0) {
            index = next[index];
        } else {
            next[i++] = 0;
        }
    }

    return next;
}

接下来我们在结合next数组和上面举的例子,看下kmp怎么玩的
例1:
S串:”abcdabce“
T串:”abce"
数组索引 0 1 2 3
字符串: a b c e
next数组: -1 0 0 0
当S中的‘e’ 与T中的’e’没有匹配时,S中的‘a’ 可以直接跟T中的’a’开始继续匹配

S串:”aabaaaaaab“
T串:”aabaac"
数组索引 0 1 2 3 4 5
字符串: a a b a a c
next数组: -1 0 1 0 1 2
当S中的‘a’ 与T中的’c’没有匹配时,S中的‘a’ 可以直接跟T中的’b’开始继续匹配,因为’aa’已经比过了,不用在重复匹配了,这里是不是一下就发现了next数组的作用。

总上当S[x]与T[y]不匹配时,S[x]可以继续与T[next[y]]进行匹配,这就是next数组提供了一些指导作用,也 可以说是分析了T串后,加速了匹配过程,让我看下代码。

/* 若匹配返回下标,若不匹配返回-1 */
int kmp(char *S, char *T)
{
    if (S == NULL || T == NULL ) {
        return -1;
    }
    int S_len = strlen(S);
    int T_len = strlen(T);
    if (S_len < T_len) {
        return -1;
    }

    int x = 0;
    int y = 0;
    int *next = get_next(T);
    /*
    时间复杂度是O(n)
    最好情况和最差情况很好推
    aaaeaaafaaat
    aaah
    类型这种情况是2n,不会存在每次x+1,y都要重头开始匹配的情况,回退的长度不回超过 x - y的最大值 n
    */
    while (x < S_len && y < T_len) {
        if (S[x] == T[y]) {
            x++;
            y++;
        } else if (next[y] == -1) { /* 此时y是0 */
            x++;
        } else {
            y = next[y];
        }
    }
    free(next);
    /* y匹配完了,才说明S包含T */
    return y == T_len ? x - y : -1;
}

这个时间复杂度理解起来比较费劲
让我们先来看while里循环
1.x++,y++ 不会超过strlen(S)
2.x++ 不会超过strlen(S)
3.y-- y累计减少次数不会超过strlen(S);代码中也有最坏情况说明。
x上升,y才会上升,y减少到0,就不会减少了,所以y累积减少次数不会超过strlen(S);
或者用x-y来表示,x上升,y减少,最大差值为strlen(S);
获取next数组的时间复杂度也类似。
若strlen(S)=M,strlen(T)=N,时间复杂度表示为O(M + N)

可以leetcode跑下对应代码。
leetcode28:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/

四、关于next数组的优化

若明白了上面说的,这里举一个例子就知道了
举个例子:
S=“aaaaaabbbbbbbb”;
T=“aaaaaac”;

对T进行分析
索引 0 1 2 3 4 5 6
T a a a a a a c
next -1 0 1 2 3 4 5
优化后 -1 0 0 0 0 0 5

优化:
当S中的第一个b,与T最后一个a比较时,可以直接跳到S的开头,不用反复做kmp代码这步操作y = next[y]一步;

核心思想:
若T[i] = T[index],则next[i] = next[index];减少重复判断,不用判断多次’a’
若T[i] != T[index],和之前一样。

下面看下代码,跟未优化的代码相比,修改了几行。

int *get_next(char *T)
{
    int len = strlen(T) + 1; // 防止越界,这里多申请一个空间
    int *next = malloc(sizeof(int) * len);
    memset(next, 0, sizeof(int) * len);
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int index = 0; /* 前缀字符串最后一个字符的索引 */

    while (i < len - 1) {
        if (T[i - 1] == T[index]) {
            /* 判断前后缀字符串最后一个字符是否一致 */
            //next[i++] = ++index;
            ++index;
            next[i] = T[i] == T[index] ? next[index] : index;
            i++;
        } else if (index > 0) {
            index = next[index];
        } else {
            next[i++] = 0;
        }
    }

    return next;
}
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值