【数据结构】串——模式匹配

目录

串的模式匹配算法

模式匹配

定义

模式匹配是指在一个"主串"(Text)中查找"模式串"(Pattern)出现的位置,并判断是否匹配。

换句话说:

  • 主串 S :你要搜索的长文本,例如 "abcabcabc"
  • 模式串 T :你要找的子串,例如 "cab"

问题:找出 T 第一次出现在 S 中的位置。

形式化描述

  • 主串:S = s1 s2 ... sn
  • 模式串:T = t1 t2 ... tm
  • 找出最小的 i,使得 S[i..i+m-1] = T[1..m]
  • 若找不到,返回 0

例子

主串:S = "abcabcabc"

模式串:T = "cab"

  • 从 S 的第 1 个字符开始比:"abc" vs "cab" → 不匹配
  • 从 S 的第 2 个字符开始比:"bca" vs "cab" → 不匹配
  • 从 S 的第 3 个字符开始比:"cab" vs "cab" → 匹配 ✅

返回位置:3(位序从1开始)

模式匹配算法分类

  1. 朴素匹配 / 暴力匹配(Naive / Brute Force)
    • 原理:从主串每个位置挨个尝试匹配模式串
    • 时间复杂度最坏 O(n·m)
    • 优点:实现简单,容易理解
    • 缺点:效率低
  2. KMP 算法
    • 利用模式串内部的重复信息,避免无谓回溯
    • 时间复杂度 O(n + m)
  3. 其他高级算法 (Boyer-Moore、Sunday 算法等)
    • 进一步优化,适合长文本匹配

朴素匹配 / 暴力匹配

朴素匹配 就是在 主串 中查找 模式串 出现的位置的最直接方法。

概念化描述

  • 主串:S = s1 s2 ... sn
  • 模式串:T = t1 t2 ... tm

问题:找出主串中第一个连续子串等于模式串的位置(位序从 1 开始)。

如果找不到,就返回 0。

简单说,就是 从头到尾挨个尝试匹配

所以也叫 暴力匹配(Brute Force),因为它不使用任何优化策略。

算法思路:

假设主串长度 n,模式串长度 m

  1. 从主串第一个字符(位序1)开始,尝试匹配模式串。
  2. 如果模式串全部匹配成功 → 返回当前位置。
  3. 如果匹配失败 → 主串起点右移 1 个字符,模式串重新从头开始。
  4. 重复步骤2,直到主串末尾。

匹配公式:

假设当前主串起点为 i,模式串索引为 j(下标从0开始):

  • 如果 S[i+j] == T[j]j++,继续比较下一个字符
  • 如果 S[i+j] != T[j]i = i + 1j = 0,从主串下一个起点重新尝试

时间复杂度:

  • 最坏情况:模式串每个字符都要和主串多次比较
  • 时间复杂度:O(n * m)
  • 空间复杂度:O(1),只需要几个计数器

所以效率低,但直观易理解。

c 复制代码
// 朴素匹配函数
int Index(HString S, HString T, int pos) {
    int i = pos - 1;  // 主串起点(位序从1开始)
    int j = 0;        // 模式串起点
    while(i < S.length && j < T.length) {
        if(S.ch[i] == T.ch[j]) { // 匹配
            i++; j++;
        } else {                 // 不匹配,回溯
            i = i - j + 1;       // 主串起点右移1
            j = 0;               // 模式串从头开始
        }
    }
    if(j == T.length)
        return i - T.length + 1; // 返回匹配位置
    else
        return 0;               // 未找到
}

**匹配过程:**示例:S = "abcabcabc",T = "cab"

初始状态:

markdown 复制代码
主串 S:a b c a b c a b c
模式串 T:c a b
匹配起点 i = 0
匹配指针 j = 0

Step 1:i=0, j=0 比较 S[0] vs T[0] → a vs c,不匹配

markdown 复制代码
主串: [a] b c a b c a b c
模式串: [c] a b
结果: × 不匹配,回溯 → i = i - j + 1 = 1, j = 0

Step 2:i=1, j=0 比较 S[1] vs T[0] → b vs c,不匹配

markdown 复制代码
主串: a [b] c a b c a b c
模式串: [c] a b
结果: × 不匹配,回溯 → i = 2, j = 0

Step 3:i=2, j=0 比较 S[2] vs T[0] → c vs c,匹配

markdown 复制代码
主串: a b [c] a b c a b c
模式串: [c] a b
结果: ✓ 匹配 → i=3, j=1

Step 4:i=3, j=1 比较 S[3] vs T[1] → a vs a,匹配

markdown 复制代码
主串: a b c [a] b c a b c
模式串: c [a] b
结果: ✓ 匹配 → i=4, j=2

Step 5:i=4, j=2 比较 S[4] vs T[2] → b vs b,匹配

markdown 复制代码
主串: a b c a [b] c a b c
模式串: c a [b]
结果: ✓ 匹配成功
匹配位置 = i - j + 1 = 3

匹配完成,模式串 "cab" 在主串 "abcabcabc" 的第 3 个字符开始出现。

  1. 模式匹配就是在主串里找子串出现的位置。

  2. 朴素匹配是最基础的方法,通过挨个尝试和回溯实现。

  3. 理解匹配过程是学习 KMP 等高效算法的前提。

KMP 算法

KMP算法是解决字符串模式匹配的高效算法。它的核心思想:

当匹配失败时,不需要把主串的起点回溯到下一个字符,而是利用 模式串自身的重复信息,直接跳过已经匹配过的字符,从而避免重复比较。

  • 主串:S = s1 s2 ... sn
  • 模式串:T = t1 t2 ... tm
  • KMP 能在 O(n + m) 时间复杂度内完成匹配,而朴素匹配最坏是 O(n·m)。

KMP的核心:Next数组(部分匹配表)

  • Next数组 记录模式串内部的 前缀和后缀的最大相等长度

    • 定义:next[j] 表示模式串 T[0...j] 的最大相等真前缀和真后缀长度(不包括整个串本身)。

    • 用途:匹配失败时,模式串向右滑动多少位,避免重复比较。

Next数组计算方法:

i 为当前考察位置,j 为前缀长度:

  1. 初始化:next[0] = 0i = 1j = 0
  2. 如果 T[i] == T[j]j++next[i] = ji++
  3. 如果 T[i] != T[j]
    • 如果 j != 0j = next[j-1]
    • 如果 j == 0next[i] = 0i++

Next数组本质上告诉我们:匹配失败时,模式串可以跳到哪儿继续匹配。

c 复制代码
void ComputeNext(char T[], int m, int next[]) {
    int i = 1, j = 0;
    next[0] = 0; // 第一个位置没有真前后缀
    while(i < m) {
        if(T[i] == T[j]) {
            j++;
            next[i] = j;
            i++;
        } else {
            if(j != 0)
                j = next[j-1];
            else {
                next[i] = 0;
                i++;
            }
        }
    }
}

KMP 算法步骤:

  1. 计算模式串的 Next 数组
  2. 主串 S 指针 i = 0,模式串 T 指针 j = 0
  3. 循环比较 S[i] 和 T[j]:
    • 相等 → i++, j++
    • 不等 → 如果 j != 0j = next[j-1];否则 i++
  4. 如果 j == m → 匹配成功,返回 i - m + 1
c 复制代码
int KMP(char S[], int n, char T[], int m) {
    int next[100];
    ComputeNext(T, m, next);

    int i=0, j=0;
    while(i<n) {
        if(S[i] == T[j]) {
            i++; j++;
        } else {
            if(j != 0)
                j = next[j-1];
            else
                i++;
        }
        if(j == m) // 匹配成功
            return i - m;
    }
    return -1; // 未找到
}

示例:S = "abcabcabc",T = "abcab"

  • 先计算 Next 数组,模式串 T = "a b c a b"

    markdown 复制代码
    索引: 0 1 2 3 4
    T:    a b c a b
    Next: 0 0 0 1 2

    Next 数组告诉我们匹配失败时,模式串跳到哪里继续比较。

  • 匹配过程

    1. 初始状态:

      markdown 复制代码
      S: a b c a b c a b c
      T: a b c a b
      i = 0 (主串指针)
      j = 0 (模式串指针)
    2. Step 1:i=0, j=0

      markdown 复制代码
      S[i]=a, T[j]=a → ✓ 匹配
      i=1, j=1
      主串: [a] b c a b c a b c
      模式串: [a] b c a b
    3. Step 2:i=1, j=1

      markdown 复制代码
      S[i]=b, T[j]=b → ✓ 匹配
      i=2, j=2
      主串: a [b] c a b c a b c
      模式串: a [b] c a b
    4. Step 3:i=2, j=2

      markdown 复制代码
      S[i]=c, T[j]=c → ✓ 匹配
      i=3, j=3
      主串: a b [c] a b c a b c
      模式串: a b [c] a b
    5. Step 4:i=3, j=3

      markdown 复制代码
      S[i]=a, T[j]=a → ✓ 匹配
      i=4, j=4
      主串: a b c [a] b c a b c
      模式串: a b c [a] b
    6. Step 5:i=4, j=4

      markdown 复制代码
      S[i]=b, T[j]=b → ✓ 匹配
      i=5, j=5 → j == T.length → 匹配成功
      匹配位置 = i - j = 0

    如果匹配失败的情况:

    假设模式串 T = "abca",S = "abcabcabc",演示失败后的跳转:

    1. 当 i=3, j=3 比较 S[3]=a, T[3]=a → 匹配 ✓
    2. i=4, j=4 比较 S[4]=b, T[4]=? → T[4]不存在 → 匹配失败
    3. 根据 Next 数组 next[3]=1 → j = 1,模式串跳到 T[1] 继续匹配
    4. i 不回退 → 保持 i=4,继续比较 S[4] 与 T[1]

    这就是 KMP 的核心:主串指针不回退,模式串指针根据 Next 数组跳跃