字符串查找匹配算法KMP

是什么:在主字符串中查找子字符串,返回第一次出现的子字符串的第一个字符索引
为什么:暴力查找字符串的时间复杂度O(n*m),KMP时间复杂度O(n+m)。 n是主串长度,m是模式串长度。

例如待查找字符串ababababca(本文统称为主串s),要匹配的字符串abababca(本文统称模式串p)

按照暴力查找的逻辑,主字符串匹配失败后接着回溯到首字符的下一个位置开始匹配,这样效率是很低的。而KMP可以在匹配失败的时候主串不回溯,只回溯模式串,这样查找速度就快了很多。

部分匹配表,即PMT(Patial Match Table),由最大前缀后缀公共字符串长度组成,例如aba的前缀是a,ab,后缀是a,ba,最大前缀后缀公共字符串是a,长度是1,所以PMT值是1

  • 前缀:除最后一个字符的子字符串
  • 后缀:除第一个字符串的字符串

因为字符串在匹配失败的时候使用的是上一个位置的next值,故将PMT值右移一位就变成了next数组,第一个位置next值设为-1。

KMP流程:

  • 先求next数组,然后匹配,匹配成功则切换到下一个字符,模式串所有字符都匹配成功则算查找成功。
  • 匹配失败则获取模式串p当前位置j的next值,将模式串当前位置回溯到next[j],如果又失败了则又回溯,当next值为-1则表示前面j个字符都匹配失败,从主串下一个字符开始匹配。

KMP的代码实现比较简单,关键是要理解算法本身。

这里我有一个疑问:匹配失败的时候主串为什么不需要回溯?怎么证明主串当前位置前n个字符都会匹配失败?关于这一点网上的博客很少有提到,就看到有博客用数学公式证明的。所以这里我从结论倒推,当主串匹配失败时(s[i]和p[j]处),主串s[i]前j个字符必定匹配失败,所以不用回溯。

匹配失败的时候能确定的信息:

  1. 匹配失败时,模式串当前位置的前j个字符和主串的前j个字符是一样的
  2. 当next值>=0时,上面的例子next[6]=4,表示ababab前4位和后4位相同,前面6位成功了,所以再匹配前4位也一定会成功,故没有必要再匹配前4位了,下次直接从第4位开始
  3. 当next值为-1,模式串当前字符前j位都匹配失败了,包括p[j],这时候主串和模式串都右移一位再比较

匹配失败时模式串右移j-next[j]位,即模式串当前位置回溯到p[next[j]]处,代码上就是j=next[j]。

注意:模式串右移只是算法逻辑上的右移,实际代码是没有右移这个操作的,当前位置回溯就相当于右移了。

还有个问题:求next数组的时候最大公共前后缀元素长度为什么要回溯?

这里可以将求next数组看作在模式串中匹配前缀的过程

KMP代码实现:

void get_next(const char *pattern, int *next) {
    size_t m = strlen(pattern);
    // pattern下标
    size_t i = 0;
    // 最大公共前后缀元素长度
    int j = -1;
    next[0] = -1;

    while (i < m) {
        if (-1 == j || pattern[i] == pattern[j]) {
            ++i;
            ++j;
            next[i] = j;
        }
        else {
            j = next[j];
        }
    }
}

int kmp(const char *str, const char *pattern, int *next) {
    int n = strlen(str);
    int m = strlen(pattern);
    if (n < m) {
        return -1;
    }
    // 主串索引
    int i = 0;
    // 模式串索引,j=-1时表示主串当前字符不匹配并且模式串不能再回溯,然后开始匹配主串下一个字符
    int j = 0;

    get_next(pattern, next);

    while (i < n && j < m) {
        if (-1 == j || str[i] == pattern[j]) {
            ++i;
            ++j;
        }
        else {
            j = next[j];
        }
    }

    if (j == m) {
        return i - j;
    }
    return -1;
}
#endif

int kmp_test() {
    int i;
    int next[20] = { 0 };
    char *text = "ababababca";
    char *pattern = "abababca";

    int idx = kmp(text, pattern, next);
    printf("match pattern : %d\n", idx);

    for (i = 0; i < strlen(pattern); i++) {
        printf("%4d", next[i]);
    }
    printf("\n");

    return 0;
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值