模式匹配

需熟练掌握KMP算法,包含next数组的计算,以及手工模拟主串和模式串的移动过程,可能在选择题中考察。

字符串模式匹配是计算机科学中的基础问题,主要是在一个主字符串中查找一个子字符串模式。

简单模式匹配算法

简单模式匹配算法通常指的是暴力方法(或称为朴素算法),其基本思想是逐个检查主字符串的每一个位置是否开始与子字符串匹配。

算法描述:

  1. 从主字符串的第一个字符开始匹配。
  2. 如果第一个字符匹配,那么匹配下一个字符,依此类推。
  3. 如果在任何时刻字符不匹配,重新从主字符串的下一个字符开始匹配。
  4. 如果子字符串完全匹配,返回主字符串中的开始位置。
  5. 如果到达主字符串的末尾都没有完全匹配的子字符串,则返回-1。
int simplePatternMatching(const char* mainStr, const char* pattern) {
    int m = strlen(mainStr);
    int n = strlen(pattern);

    // 如果主字符串的长度小于模式字符串的长度,直接返回-1
    if (m < n) return -1;

    for (int i = 0; i <= m - n; i++) {
        int j;
        for (j = 0; j < n; j++) {
            if (mainStr[i + j] != pattern[j]) {
                break;
            }
        }
        // 如果j等于模式串的长度,说明已经找到匹配
        if (j == n) return i;
    }
    return -1;  // 没有找到匹配
}

这个简单模式匹配算法的时间复杂度是 $O(mn)$ ,其中 $m$ 是主字符串的长度, $n$ 是模式字符串的长度。

KMP算法

与简单模式匹配算法不同,KMP算法在发现不匹配的字符时能够避免不必要的比较,从而提高效率。

基本原理

简单模式匹配时间复杂度过高,因为每一次匹配失败都得从下一个位置重新开始。这就没有充分应用模式串的特性,比如对于字符串 abcdabc,我们可以发现:

  • 其前缀包含a, ab, abc, abcd
  • 起后缀包含c, bc, abc, dabc

abc是在其前缀和后缀中都包含的部分,在字符串匹配时,我们可以充分利用该信息,匹配失败时,我们可以不用从下一个位置开始,而是从模式串内部的某个位置开始。

KMP算法就是基于这个思想,其核心是一个称为“部分匹配表”或“前缀函数”的辅助数组(通常称为next数组),该数组用于确定当模式串与主串不匹配时应该如何有效地移动模式串。

算法描述:

  • 根据模式串计算next数组。
  • 使用next数组,对主串和模式串进行比较。
  • 在发现不匹配的情况下,利用next数组调整模式串的位置。

next数组计算

next数组的计算方式如下:

$$next[j] = \begin{cases} 0 &\text{if } j = 1 \\ max\{k \text{ | } 1 \lt k \lt j\ \text{ and }\ p_1 \cdots p_{k} = p_{j-k+1} \cdots p_{j}\} &\text{if k exists}\\ 0 &\text{if k not exists} \end{cases}$$

如果模式串为pattern,用通俗的话来说,next[j]就是字串pattern[0:k](pattern的前k个字符构成的子串)的最长相同前缀和后缀的长度。

根据next数组调整位置

i表示主串当前下标,用j表示模式串当前下标:

  • 如果main[i] == pattern[j],将ij向后向后移动一位。
  • 如果main[i] != pattern[j]
    • 如果j == 0,将i向后移动一位。
    • 如果j != 0,将j移动到next[j-1]

以下图为例,说明主串 “ababcabcabababd” 和 模式串 “ababd” 的匹配过程。

index
0
1
2
3
4
char
a
b
a
b
d
next
0
0
1
2
0
字符串 "ababd" 对应的next数组
a
b
a
b
c
a
b
c
a
b
a
b
a
b
d
string
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
a
b
a
b
d
i
j
在 i = 4, j = 4 时匹配失败,
next[j-1] = next[3] = 2,
移动 j = 2
a
b
a
b
d
j
在 i = 4, j = 2 时匹配失败,
next[j-1] = next[1] = 2,
移动 j = 0
a
b
a
b
d
j
在 i = 4, j = 0 时匹配失败,
j=0,移动 i = i+1 = 5
a
b
a
b
c
a
b
c
a
b
a
b
a
b
d
string
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
i
a
b
a
b
d
j
在 i = 5,j = 0 时匹配失败,
j=0,移动 i++, j++
i = 6, j = 1
pattern
pattern
void computeNextArray(const char* pattern, int m, int* next) {
    int len = 0;
    next[0] = 0;
    int i = 1;

    while (i < m) {
        if (pattern[i] == pattern[len]) {
            len++;
            next[i] = len;
            i++;
        } else {
            if (len != 0) {
                len = next[len - 1];
            } else {
                next[i] = 0;
                i++;
            }
        }
    }
}

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

    int next[n];
    computeNextArray(pattern, n, next);

    int i = 0, j = 0;
    while (i < m) {
        if (pattern[j] == mainStr[i]) {
            i++;
            j++;
        }

        if (j == n) {
            return i - j;
        } else if (i < m && pattern[j] != mainStr[i]) {
            if (j != 0)
                j = next[j - 1];
            else
                i++;
        }
    }

    return -1; // 没有找到匹配
}

视频讲解