12-串1 KMP 串的模式匹配

234 阅读2分钟

题目描述

给定两个由英文字母组成的字符串 String 和 Pattern,要求找到 Pattern 在 String 中第一次出现的位置,并将此位置后的 String 的子串输出。如果找不到,则输出“Not Found”。

本题旨在测试各种不同的匹配算法在各种数据情况下的表现。各组测试数据特点如下:

  • 数据0:小规模字符串,测试基本正确性;
  • 数据1:随机数据,String 长度为 105,Pattern 长度为 10;
  • 数据2:随机数据,String 长度为 105,Pattern 长度为 102
  • 数据3:随机数据,String 长度为 105,Pattern 长度为 103
  • 数据4:随机数据,String 长度为 105,Pattern 长度为 104
  • 数据5:String 长度为 106,Pattern 长度为 105;测试尾字符不匹配的情形;
  • 数据6:String 长度为 106,Pattern 长度为 105;测试首字符不匹配的情形。

输入格式

输入第一行给出 String,为由英文字母组成的、长度不超过 106 的字符串。第二行给出一个正整数 N(≤10),为待匹配的模式串的个数。随后 N 行,每行给出一个 Pattern,为由英文字母组成的、长度不超过 105 的字符串。每个字符串都非空,以回车结束。

输出格式

对每个 Pattern,按照题面要求输出匹配结果。

输入样例

abcabcabcabcacabxy
3
abcabcacab
cabcabcd
abcabcabcabcacabxyz

输出样例

abcabcacabxy
Not Found
Not Found

题解

完整代码(AC):

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

void BuildMatch(char *pattern, int *match, int m)
{
    int i, j;
    match[0] = -1;
    for (j = 1; j < m; j++) {
        i = match[j - 1];
        while (i >= 0 && pattern[i + 1] != pattern[j])
            i = match[i];
        if (pattern[i + 1] == pattern[j]) match[j] = i + 1;
        else match[j] = -1;
    }
}

char *KMP(char *string, char *pattern)
{
    int n = strlen(string);
    int m = strlen(pattern);

    if (n < m) return NULL;

    int s, p;
    int *match = (int*)malloc(sizeof(int) * m);
    BuildMatch(pattern, match, m);
    s = p = 0;
    while (s < n && p < m) {
        if (string[s] == pattern[p]) { s++; p++; }
        else if (p > 0) p = match[p - 1] + 1;
        else s++;
    }
    if (p == m) return string + s - m;
    else return NULL;
}

int main()
{
    int i, N;
    char string[1000001], pattern[100001];
    char *p;
    scanf("%s", string); getchar();
    scanf("%d", &N); getchar();
    for (i = 0; i < N; i++) {
        scanf("%s", pattern); getchar();
        p = KMP(string, pattern);
        if (p) printf("%s\n", p);
        else printf("Not Found\n");
    }
    return 0;
}

理解 KMP 算法

在解决串的模式匹配问题中,最直接的思路就是一个一个字符比对,这样子的时间复杂度是 O(N2)O(N^2) ,面对长文本的匹配是低效的。于是需要学习一种非常巧妙的模式匹配算法,能够在 O(N+M)O(N+M) 的时间复杂度内完成串的模式匹配,即 KMP 算法。

在最简单的匹配算法中,是一个一个进行比对,发现有不同的就重新回退比较下一个字符所引导的字符串。导致这种算法时间效率较低的原因是比对过程中指向 string 的下标指针也经历了回退。 KMP 算法就是做到了比对全程指向 string 下标的指针完全没有回退,一路比下去,只有指向 pattern 下标的指针经历了回退。

做到这一点的关键在于,每一次比对失败回退时,pattern 指针不回退到原点,而是找到尾端与开头端一部分重复的那部分,比对失败前这部分是与目前 string 以比对部分匹配上的,只需要在 pattern 出现失败前的末端重新找到已经与 string 匹配上的一部分接着往 string 指针后面比对即可。这样就做到了全程没有让 string 的指针经历回退。

在具体实现这一算法时,需要一个与模式串长度相同的数组 match 来保存 pattern 指针在比对某一个字符失败时应该回退到的位置。match 数组的具体含义是:从 pattern 字符串的头到当前位置这样一个串里出现首尾相同的最长子串的那最后一个字符的下标,如果不存在就为 -1

Pasted image 20220829141356.png

有了 match 数组,就可以写出完整的 KMP 算法:

  • 首先获取待匹配串与模式串的字符串长度,如果模式串比待匹配串长显然直接退出,无匹配子串。
  • 然后建立 match 数组,至于怎么建立,后面再说,现在姑且认为我们已经生成了我们需要的存有每次指针回退下标位置的数组
  • 进入比对循环,退出条件是 string 扫描完或者 pattern 扫描完
    • 如果当前字符比对成功,指针同时后移一位比对下一个字符
    • 如果发现字符不同(但要注意 p 不能为 0 ,不然 match 数组越界),根据当前 match 函数所告知的回退下标回退 p ,进入下一轮比对
    • 如果 p 是 0 的时候(即模式串第一个字符就不匹配),s 后移一位,进入下一轮比对
  • 循环退出后
    • 如果 p == m ,即 pattern 完全比对完毕,返回待匹配串内匹配到的子串所在第一个元素下标
    • 否则说明 pattern 没有比对完就退出了,string 比对到尽头都没匹配成功,返回没有匹配到模式串

Pasted image 20220829135442.png

明白 KMP 的算法实现后,还剩下最后一个问题要解决,即如何构造 match 数组。

BuildMatch 函数的实现带有一种递归或者说迭代的思想。假设前面的 match 都生成好了,现在我要生成下标为 j 的 match 元素内容,我应该去比对 match 中下标为 j 的元素和下标为 match[j - 1] + 1 的元素是否相等,如果相等,说明首尾相同的串可以往后追加一个新元素,这个相同串变长,因为我们需要找到最长的首尾相同的子串。

前后两个串都追加过一个元素后,需要从中间两个对称位置的元素来比对是否相等。但是,关键在这里,不可能中间对称位置元素相等继续使这个子串变长,如果是这样的话,原来的 match[j - 1] 就不是现在这个数了,他会比原来多一个,已经限制了不会再长。(有些抽象)

于是就有如下图中间所示的语句,一旦发现追加成功,那么现在我要求的 match[j] 就等于上一个位置处的 match[j -1] 加一。这也是最后迭代退出的条件之一。

如果我们发现追加失败,即 pattern[match[j - 1] + 1] != pattern[j] ,就再找子串的子串,运用上述类似的方法直到找到可以首尾相等的子串。然后更新 match[j] 。

Pasted image 20220829142845.png

如果上面的抽象过程很难理解,看代码实现其实会好理解一些:

  • 首先我们知道模式串第一个元素的 match[0] 肯定是 -1
  • 然后开始从 1 到 m - 1 迭代生成各个位置上的 match[j]
    • 首先用一个 i 记录一下 match[j - 1] 使后面的代码简洁
    • 进入找子串的内循环,循环条件是 i >= 0 && pattern[i + 1] != pattern[j]
      • 不断迭代(递归)去找到匹配的那个 i
      • i = match[i];
    • 如果一开始就发现能追加成功其实内循环是不跑的
    • 退出循环后如果匹配成功,就更新我们需要的 match[j] 为 i + 1(即 match[j - 1] + 1 )
    • 否则说明 i 都退到 - 1 了还没匹配上,那就说明没有首尾相同的子串,match[j] 等于 -1

Pasted image 20220829135527.png

熟练算法,记住就好,难理解也没关系,以后某个时候就理解了,现在初学首先要会用。