程序员面试宝典

一站式面试准备平台

返回分类
algorithms中级

字符串匹配算法

深入理解 KMP 算法的原理、前缀函数的作用、以及为什么 KMP 比暴力匹配快

2026-04-15
阅读时间: 8分钟

字符串匹配算法

KMP 是面试中最常考的字符串算法。关键不是背代码,而是理解为什么 KMP 不用回退文本指针,以及前缀函数到底在表达什么

面试考察点

面试官通过这道题想考察:
1. 你是否能解释暴力匹配的缺点
2. 你是否能理解 KMP 的核心思想
3. 你是否能解释前缀函数的含义
4. 你是否能手写 KMP

一、暴力匹配

1.1 暴力匹配的问题

面试官追问:"暴力匹配为什么慢?"

文本 T: "ABABABCABAABABAC"
模式 P: "ABABAC"

暴力匹配:

T[0] 和 P[0] 匹配 ✓
T[1] 和 P[1] 匹配 ✓
T[2] 和 P[2] 匹配 ✓
...
T[5] 和 P[5] 不匹配!

怎么办?暴力解法:
T 的指针回退到 T[1]
从 P[0] 重新开始

T:    A B A B A B C A B A A B A B A C
P:    A B A B A C
       ↑ 不匹配!

最坏情况:
T = "AAAAAAAAAB"
P = "AAAAB"

每次都要回退 T 的指针
时间复杂度:O(n × m)

1.2 暴力匹配为什么浪费?

关键观察:

T:    A B A B A B C A B A A B A B A C
P:    A B A B A C
                 ↑
           匹配到 P[5] 时失败

T 的指针回退到了 T[1]

但我们知道:
- T[0..5] = "ABABAB"
- P[0..5] = "ABABAC"
- 已经匹配了 T[1..4] = P[0..3]

为什么要回退到 T[1]?
从 T[1] 开始意味着 T[1..4] 要重新和 P 比较

但 T[1..4] = "BABA",而 P[0..3] = "ABAB"
它们根本不可能匹配!(开头不同)

暴力匹配浪费了已知的信息!

二、KMP 算法

2.1 KMP 的核心思想

面试官追问:"KMP 怎么避免回退?"

KMP 的核心:不回退 T 的指针!

利用已匹配的信息:
当 P[5] 匹配失败时,我们知道 T[0..5] = "ABABAB",P = "ABABAC"

已经匹配的 "ABABA" 有什么特点?
- 前缀 "ABA" 和后缀 "ABA" 相同!

所以可以这样:

T:    A B A B A B C A B A A B A B A C
P:    A B A B A C
              ↑
        P[5] 匹配失败

T[0..5] = "ABABAB"
前缀 "ABA" 和后缀 "ABA" 相同!

所以直接把 P 向右滑到:

T:    A B A B A B C A B A A B A B A C
P:        A B A B A C
          ↑       ↑
        P[0]   原来的 P[2]

让 P[0] 对齐 T[2](跳过已知的匹配)

关键:不需要回退 T 的指针!

2.2 前缀函数(核心)

面试官追问:"什么是前缀函数?"

前缀函数(Failure Function):求模式串每个位置的最长相等前后缀

举例:

P = "ABABAC"

求每个位置的最长相等前后缀(不包括本身):

位置 0 ('A'):没有前缀后缀 → 0
位置 1 ('AB'):没有相等前后缀 → 0
位置 2 ('ABA'):前缀"A" = 后缀"A" → 1
位置 3 ('ABAB'):前缀"AB" = 后缀"AB" → 2
位置 4 ('ABABA'):前缀"ABA" = 后缀"ABA" → 3
位置 5 ('ABABAC'):没有相等前后缀 → 0

前缀函数:π = [0, 0, 1, 2, 3, 0]

有什么用?
当匹配失败时,查前缀函数,
确定 P 应该滑动到哪里!

2.3 KMP 匹配过程

T = "ABABABCABAABABAC"
P = "ABABAC"
π = [0, 0, 1, 2, 3, 0]

匹配:

Step 1: T[0..4] = "ABABA",P[0..4] = "ABABA" 匹配
        T[5] = 'B',P[5] = 'C' 不匹配

        T:    A B A B A B C A B A A B A B A C
        P:    A B A B A C
                          ↑
                     π[5] = 0

Step 2: P 滑动,P[0] 对齐 T[2]

        T:    A B A B A B C A B A A B A B A C
        P:        A B A B A C
                  ↑       ↑
              P[0]    原来的 P[2]

Step 3: 继续匹配...

为什么这样滑动是对的?
因为 π[5] = 0 表示没有相等前后缀
所以 P[0] 不能和 T[2] 之后的任何位置匹配
(因为 T[0..4] 的后缀不可能是 P 的前缀)

三、面试总结

KMP 核心要点

┌─────────────────────────────────────────────────┐
│                    KMP 核心                       │
├─────────────────────────────────────────────────┤
│                                                  │
│  核心思想:不回退 T 的指针,利用已匹配的信息       │
│                                                  │
│  前缀函数:                                      │
│  - 位置 i 的最长相等前后缀长度                   │
│  - 匹配失败时,确定 P 滑动的位置                 │
│                                                  │
│  时间复杂度:O(n + m)                            │
│                                                  │
│  vs 暴力匹配:                                   │
│  - 暴力:回退 T 的指针,O(nm)                   │
│  - KMP:不回退,利用信息,O(n+m)               │
│                                                  │
└─────────────────────────────────────────────────┘

面试能加分的回答

1. 能解释为什么 KMP 快
   "暴力匹配回退了 T 的指针,浪费了已知信息,
    KMP 通过前缀函数,让 P 滑动到正确的位置"

2. 能解释前缀函数
   "前缀函数表示位置 i 的最长相等前后缀,
    当匹配失败时,查这个值就知道 P 该滑到哪里"

3. 能手写前缀函数
   "用递推:如果 s[i] == s[π[i-1]],则 π[i] = π[i-1] + 1"

相关题目

相关标签