串的模式匹配算法

160 阅读4分钟

在主串中查找定位子串问题(模式匹配)是串中最重要的操作之一,一般而言有两种匹配方式

BF算法(朴素的模式匹配算法)

模式串和主串进行逐位比较, 如果匹配成功指针同时+1, 如果匹配失败, 则需要将指针回溯。最后确定主串中所含子串第一次出现的位置,这里的子串也称为模式串,如果匹配失败返回-1.其实就是朴素的暴力做法,主串和模式串逐个字符进行比较。

当以0下标开始匹配时,代码如下:

int Index_DF_one(string s, string t) {  // s是主串,t是模式串
    int i = 0, j = 0;                   //都从1开始存字符
    while (i < s.size() && j < t.size()) {
        if (s[i] == t[j])
            i++, j++;  //主串和子串依次匹配下一个字符
        else {         //匹配失败,主串,子串指针回溯
            i = i - j + 1;  //主串回到上一次开始匹配的子覅的下一字符,这里减1即可
            j = 0;  //子串回到起点1
        }
    }
    if (j >= t.size())
        return i - t.size();  //匹配成功返回匹配的第一个字符的下标,如果要返回具体位置就+1.
    else
        return -1;  //匹配不成功 
}

当然也可以以1为下标开始。

最坏的情况,时间复杂度是O(nm),而平均时间复杂度是O(nm)/2,实际上也就是O(n*m).BF算法需要多次回溯,效率低下,所以有了效率高的KMP算法。

KMP算法

利用已经部分匹配的结果而加快模式串的滑动速度,这样主串的指针i不必回溯,子串指针j回溯。

我们不用依次匹配,而是寻找到模式串中重复的部分。因为在串的匹配过程中,主串与模式串匹配到某个位置k时,模式串中有相同的字符片段,两个字符片段都在主串与模式串相互匹配了,当在k位时,匹配失败,这时候将模式串右移,移动到第二个相同的字符片段处,模式串的比较指针j回退到模式串[0, j-1]之间的最长公共前后缀的长度的那个位置(即指向最长前缀的下一位).

image.png

比如对于一个序列:excited 它的前缀包括:e, ex, exc, exci, excit, excite 它的后缀包括:d, ed, ted, ited, cited, xcited

怎么知道指针j该移动到哪个位置,就引入next[]数组。next[]数组其实计算模式串每个位置的最长公共前后缀的长度.也就是说如果我们预先计算好模式串中每个点之前的子串的最大公共前后缀长度,并把它作为一个和模式串等长的数组存起来,那么之后可以更快的匹配。这个数组通常就是next数组,因为它表征着在该位置失配后下一个需要匹配的模式串的位置。

j 表示指向最长公共前缀的最后一位(前缀指针), i表示指向最长公共后缀的最后一位(后缀指针). 如果i 和 j 相等则前后缀指针同时+1, 如果不相等则需要回退前缀指针j 到上一个最长的前后缀长度的位置, 即 next[j-1]. 此时再比较j 和 i. 如果匹配则此时的j+1就是最长公共前后缀的长度, 如果还是不匹配则 前缀指针j 继续回退, 直到 j 和 i 相等或者 回退到起点0. 最后前缀指针 j + 1的值就表示next[0, i] 之间的最长前后缀的长度, 它意义也表示指向最长前缀的下一位

求next数组的方法,下标都从0开始,注意ne[0] = 0啊

  for (int i = 1, j = 0; i < n; i++) {
        while (j > 0 && p[i] != p[j]) j = ne[j - 1];  //回退至前一位的next位置
        if (p[i] == p[j]) j++;

        ne[i] = j;
    }

kmp代码

for (int i = 0, j = 0; i < m; i++) {
        while (j > 0 && p[j] != s[i]) j = ne[j - 1];
        if (s[i] == p[j]) j++;

        if (j == n) {
            printf("%d ", i - n + 1);
        }
    }

实在记不住可以背下来做模板。

还有从1为下标开始的:

   //模板串p与自身进行匹配,发现相等的前缀和后缀,从而计算出ne数组
    // ne[1]=0, 所以i从2开始
    for (int i = 2, j = 0; i <= n; i++) {
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j++;
        ne[i] = j;
    }

    //模板串p 与长串s进行匹配
    //输出数据是要求从0开始计数
    for (int i = 1, j = 0; i <= m; i++) {
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j++;
        if (j == n) {
            printf("%d ", i - n);
        }
    }