第 4 章 串(KMP 模式匹配)
本章地位:串是内容受限的线性表,在 408 中分值不高(选择 0~2 分),但next 数组手算几乎必考选择题——给模式串求 next / nextval、问匹配趟数或比较次数,是数据结构中最稳定的「送分计算题」。核心只有两件事:① 把 next 的定义「最长相等前后缀长度 + 1」背到条件反射;② 用两个具体模式串把 BF 的回溯和 KMP 的右滑各走一遍。本章所有手算结果都逐步列核对表。
4.1 串的定义与存储
4.1.1 基本概念:空串、子串与位置
② 子串在主串中的位置:子串第一个字符在主串中的位序;
③ 两个串相等:当且仅当它们的长度相等且对应位置上的字符都相同。
例:\(s=\) 'ababcabcacbab'(长 13)中,子串 'abcac' 出现的位置是 6(其首字符 a 是主串第 6 个字符)。
② 'abc' 与 'abc '(尾部多一个空格)不相等——长度不同即否;
③ 子串必须连续:'ac' 不是 'abc' 的子串(跳过 b 的叫子序列,不是子串)。
下列关于串的叙述中,错误的是( )
A. 空串是任何串的子串 B. 串 'abc' 的非空子串共有 6 个 C. 空格串的长度为零 D. 两个串相等当且仅当二者长度相等且对应位置字符相同
查看解答
C。逐项分析:
A 对:按定义子串包含空串(“任意个连续字符”含零个);空串也是任何串的真子串。
B 对:'abc' 的非空子串 a、b、c、ab、bc、abc 共 6 个 \(=\frac{3\times4}{2}\) ✓。
C 错:空格串由一个或多个空格字符组成,长度等于空格个数;长度为零的是空串。
D 对:这正是串相等的定义(先比长度,再逐位比字符)。
判断正误:(1) 'ab' 是 'aabb' 的子串;(2) 串 ' data '(首尾各 1 个空格)与 'data' 相等;(3) 空串是线性结构。
查看答案
(1) 错:'aabb' 的长度为 2 的子串只有 aa、ab、bb,'ab' 之后无法再取出连续的 'ab'(子序列才可以)。
(2) 错:长度 6 ≠ 4,空格也是字符。
(3) 对:串(含空串)是元素受限(字符)的线性表,逻辑结构为线性结构。
4.1.2 存储结构:顺序(定长 / 堆分配)与块链
- 定长顺序存储:用固定长度数组存字符,多余位置补 '\0' 或另设 length 字段记录实际长度——超过预定义长度的串值被截断(“截断”是定长串特有现象);
- 堆分配存储:仍用一组地址连续的存储单元,但空间在程序执行时按串长动态分配(malloc / realloc),克服截断;
- 块链存储:链式存储,每个结点可存多个字符(结点中字符个数称结点大小),最后一个结点不满时用 '#' 或空格填充。
// 定长与堆分配顺序串的类型定义对比
#define MAXLEN 255
typedef struct { // ① 定长顺序串:空间固定,超长截断
char ch[MAXLEN];
int length;
} SString;
typedef struct { // ② 堆分配串:运行时按需申请
char *ch; // 指向动态分配的存储区
int length;
} HString;
设字符占 1 B、指针占 4 B。用结点大小为 4 的块链存储一个长度为 100 的串,共需多少结点?实际占用多少字节?存储密度是多少?
查看解答
结点数 \(=\lceil100/4\rceil=25\) 个;每结点实际占 \(4+4=8\) B,共 \(25\times8=200\) B。
\[ \text{密度}=\frac{100\times1}{200}=50\%\quad\Big(=\frac{4}{4+4}\Big)\ \checkmark \]
与公式一致:结点整除填满时密度只取决于结点大小,与串长无关。若改用结点大小 80:\(\lceil100/80\rceil=2\) 个结点占 \(2\times84=168\) B,密度 \(\frac{100}{168}\approx60\%\)(最后一个结点浪费 60 个字符位,短串时大结点反而不划算)。
(1) 串 'abcde' 的非空子串有几个?含空串呢? (2) 定长顺序串 'abcdefghij'(MAXLEN = 8)实际存下的是什么?
查看答案
(1) 非空子串 \(\frac{5\times6}{2}=15\) 个(验证:按长度 \(5+4+3+2+1=15\) ✓);含空串共 16 个。
(2) 只能存前 8 个字符 'abcdefgh',后面 'ij' 被截断——这是定长顺序存储的固有缺陷,堆分配存储不存在此问题。
4.1.3 串的基本运算:与线性表的差别
4.2 BF 简单模式匹配 方法
4.2.1 算法思想:失配后主串指针回溯
4.2.2 最坏时间复杂度 \(O(nm)\) 及验证
// BF 简单模式匹配:位序从 1 开始,返回 T 在 S 中首次出现的位置,失败返回 0
int BF(char S[], char T[], int n, int m) {
int i = 1, j = 1;
while (i <= n && j <= m) {
if (S[i] == T[j]) {
++i; ++j; // 当前字符相等,继续比较后继
} else {
i = i - j + 2; // 主串指针回溯到本趟起点的下一位置
j = 1; // 模式串回到第 1 个字符
}
}
if (j > m) return i - m; // 匹配成功:本趟起点
else return 0; // 匹配失败
}
主串 \(S=\) 'aaaaaaaaab'(长 10),模式串 \(T=\) 'aaab'(长 4)。求 BF 算法匹配成功前的总比较次数。
查看解答
起点可取 \(10-4+1=7\) 个;每趟前 3 个 'a' 全部配对,第 4 次比较 \(T_4=\) 'b' 与主串字符:起点 1~6 处均为 'a',失配(各 4 次);起点 7 时 \(S_7\)a✓、\(S_8\)a✓、\(S_9\)a✓、\(S_{10}\)b✓ 成功(4 次)。
\[ 7\times4=28\ \text{次}=(n-m+1)\times m\ \checkmark \]
套路总结:'aaa…a b' 型主串 + 'a…ab' 型模式 = BF 最坏输入,每趟必比满 m 次再失败。
主串长 \(n=100\)、模式串长 \(m=5\)。分别求 BF 算法「首趟即匹配成功」与「最坏情况」的比较次数。
查看答案
最好:\(m=5\) 次(一次比到底)。最坏:\((100-5+1)\times5=96\times5=480\) 次 ✓。
4.3 KMP 模式匹配 高频考点
4.3.1 next 数组:定义与手算
演示一:\(t=\) 'abaabc'。逐步核对表:
| j | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| t[j] | a | b | a | a | b | c |
| 前缀 t[1..j−1] | — | a | ab | aba | abaa | abaab |
| 最长相等前后缀 | — | 无 | 无 | a | a | ab |
| next[j] | 0 | 1 | 1 | 2 | 2 | 3 |
抽查核对:j = 6 时看 'abaab',头 2 个 'ab' 与尾 2 个 'ab' 相等(长 2),头 3 个 'aba' 与尾 3 个 'aab' 不等,故 next[6] = 2 + 1 = 3 ✓;j = 5 时 'abaa' 头 'a' = 尾 'a'(长 1),头 'ab' ≠ 尾 'aa',故 next[5] = 2 ✓。
演示二:\(t=\) 'ababaa'。逐步核对表:
| j | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| t[j] | a | b | a | b | a | a |
| 前缀 t[1..j−1] | — | a | ab | aba | abab | ababa |
| 最长相等前后缀 | — | 无 | 无 | a | ab | aba |
| next[j] | 0 | 1 | 1 | 2 | 3 | 4 |
抽查核对:j = 6 时 'ababa',头 'aba' = 尾 'aba'(长 3),故 next[6] = 4 ✓;j = 5 时 'abab',头 'ab' = 尾 'ab'(长 2),故 next[5] = 3 ✓。
4.3.2 nextval:next 数组的再优化
对上述两串求 nextval(t[next[j]] 一行是比较依据,"—"表示 next[j]=0 无从比较,nextval[1] 恒为 0):
| t = 'abaabc' | j=1 | j=2 | j=3 | j=4 | j=5 | j=6 |
|---|---|---|---|---|---|---|
| next[j] | 0 | 1 | 1 | 2 | 2 | 3 |
| t[j] 与 t[next[j]] | — | b≠a | a=a | b=b | a≠b | c≠a |
| nextval[j] | 0 | 1 | 0 | 1 | 2 | 3 |
| t = 'ababaa' | j=1 | j=2 | j=3 | j=4 | j=5 | j=6 |
|---|---|---|---|---|---|---|
| next[j] | 0 | 1 | 1 | 2 | 3 | 4 |
| t[j] 与 t[next[j]] | — | b≠a | a=a | b=b | a=a | a≠b |
| nextval[j] | 0 | 1 | 0 | 1 | 0 | 4 |
核对示例:'abaabc' 的 j=3,next[3]=1 且 t[3]='a'=t[1]='a',故 nextval[3]=nextval[1]=0 ✓;j=4,next[4]=2 且 t[4]='b'=t[2]='b',故 nextval[4]=nextval[2]=1 ✓。'ababaa' 的 j=6,next[6]=4 但 t[6]='a'≠t[4]='b',故 nextval[6]=next[6]=4 ✓。
4.3.3 匹配过程演练:主串指针不回溯
主串 \(S=\) 'ababcabcacbab'(\(n=13\)),模式串 \(T=\) 'abcac'(\(m=5\))。先手算 next:
| j | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| t[j] | a | b | c | a | c |
| 最长相等前后缀 | — | 无 | 无 | 无 | a |
| next[j] | 0 | 1 | 1 | 1 | 2 |
逐趟匹配(每趟给起点与失配位):
- 第 1 趟(起点 1):\(S_1\)a=\(T_1\) ✓,\(S_2\)b=\(T_2\) ✓,\(S_3\)a≠\(T_3\)c 失配(\(i=3,j=3\))→ \(j=\mathrm{next}[3]=1\),\(i\) 不动。本趟 3 次。
- 第 2 趟(起点 3):\(S_3\)a ✓,\(S_4\)b ✓,\(S_5\)c ✓,\(S_6\)a ✓(比到 \(T_4\)),\(S_7\)b≠\(T_5\)c 失配(\(i=7,j=5\))→ \(j=\mathrm{next}[5]=2\),\(i\) 不动。本趟 5 次。
- 第 3 趟(起点 6):\(S_7\)b=\(T_2\) ✓,\(S_8\)c=\(T_3\) ✓,\(S_9\)a=\(T_4\) ✓,\(S_{10}\)c=\(T_5\) ✓ → \(j=6>m\),成功,位置 \(=i-m=11-5=6\)(\(S[6..10]=\) 'abcac' ✓)。本趟 4 次。
总比较 \(3+5+4=12\) 次 ✓;主串指针 \(i\) 的轨迹 \(1\to2\to\cdots\to11\) 单调前进、从不回退——这正是 KMP 的标志。同一对串 BF 需比较 16 次才成功。
4.3.4 KMP 代码、复杂度与易错点
// 求 next 数组 + KMP 匹配(位序从 1 开始,next[1] = 0)
void get_next(char T[], int next[], int m) {
int i = 1, j = 0;
next[1] = 0; // 约定
while (i < m) {
if (j == 0 || T[i] == T[j]) {
++i; ++j;
next[i] = j; // t[1..i-1] 最长相等前后缀 + 1
} else
j = next[j]; // 前缀指针回退(递推求法)
}
}
int KMP(char S[], char T[], int next[], int n, int m) {
int i = 1, j = 1;
while (i <= n && j <= m) {
if (j == 0 || S[i] == T[j]) {
++i; ++j; // 主串指针只前进,从不回溯
} else
j = next[j]; // i 不动,模式串右滑
}
if (j > m) return i - m; // 成功:返回起点位序
else return 0;
}
模式串 \(t=\) 'ababab'(位序从 1)。(1) 求 next 数组;(2) 在此基础上求 nextval 数组。
查看解答
(1) 逐步算最长相等前后缀:'a' 无;'ab' 无;'aba' 有 'a'(长 1);'abab' 有 'ab'(长 2);'ababa' 有 'aba'(长 3)。各 +1:
next = 0 1 1 2 3 4 (核对 j=6:'ababa' 头尾 'aba' 相等,3+1=4 ✓)
(2) 逐位判断 \(t[j]\) 与 \(t[\mathrm{next}[j]]\):j=2:b≠a → 1;j=3:a=a → nextval[1]=0;j=4:b=b → nextval[2]=1;j=5:a=a → nextval[3]=0;j=6:b=b → nextval[4]=1。
nextval = 0 1 0 1 0 1 ✓(周期串的 nextval 呈 0、1 交替,验证了「重复字符必再失配」的直觉)
主串 \(S=\) 'abababaa'(\(n=8\)),模式串 \(T=\) 'ababaa'(\(m=6\)),用 KMP(next = 0 1 1 2 3 4)匹配。问共几趟、比较总次数多少?并与 BF 对比。
查看解答
第 1 趟(起点 1):\(S_1\cdots S_5\) 与 \(T_1\cdots T_5\) 全配对(5 次),\(S_6\)b≠\(T_6\)a 失配(1 次),\(i=6,j=6\) → \(j=\mathrm{next}[6]=4\),共 6 次。
第 2 趟(起点 \(6-4+1=3\)):\(S_6\)b=\(T_4\)b ✓,\(S_7\)a=\(T_5\)a ✓,\(S_8\)a=\(T_6\)a ✓ → 成功,位置 3(\(S[3..8]=\) 'ababaa' ✓),共 3 次。
共 2 趟、9 次比较 ✓。BF:起点 1 比 6 次失配,起点 2 比 5 次失配,起点 3 比 6 次成功,共 17 次——KMP 省下的正是主串指针反复回溯的重复劳动。
位序从 1 开始。下列关于 next 数组的叙述中,正确的是( )
A. next[j] 等于 t[1..j−1] 中最长相等前后缀的长度 B. 对任何模式串都有 next[1]=0、next[2]=1 C. 存在模式串使 next[j]=j D. nextval[j] 一定小于 next[j]
查看解答
B。逐项分析:
A 错:next[j] = 最长相等前后缀长度 +1(如 'ababaa' 的 next[6]=4,而最长前后缀长 3)。
B 对:均为约定值,与串内容无关。
C 错:t[1..j−1] 的真前缀至多长 \(j-2\),故 next[j] ≤ \(j-1\),取不到 \(j\)('aaaa' 也只有 next[4]=3)。
D 错:t[next[j]]≠t[j] 时二者相等(如 'abaabc' 的 j=5:next[5]=nextval[5]=2)。
分别求模式串 'aaaaa' 与 'ababa'(位序从 1)的 next 与 nextval 数组。
查看答案
'aaaaa':前后缀逐层相等,next = 0 1 2 3 4;因相邻字符全同,nextval 全部回溯到源头,nextval = 0 0 0 0 0(核对 j=5:t[5]=a=t[next[5]=4]=a → nextval[4] → … → nextval[1]=0 ✓)。
'ababa':next = 0 1 1 2 3;nextval:j=2 b≠a→1,j=3 a=a→0,j=4 b=b→1,j=5 a=a→0,即 nextval = 0 1 0 1 0。
4.4 章末自测 真题风格
限时 40 分钟,先做后看答案。难度:★★ 基础 / ★★★ 强化 / ★★★★ 冲刺。所有 next / nextval 手算结果务必逐位核对(用 j=末位抽查一次)。
设 s₁ 为空串,s₂ = ' '(2 个空格)。下列说法正确的是( )
A. s₁ 与 s₂ 相同 B. s₂ 的长度为 0 C. s₁ 长度为 0 而 s₂ 长度为 2 D. s₂ 是空串
查看答案
C。空格也是字符:空串长 0,空格串长 = 空格个数;二者只在「长度」上都与 2 无关的 A、B、D 全错。
串 'abcde' 的子串个数(含空串)共 \(\underline{\hspace{1.5cm}}\) 个。
查看答案
\(\frac{5\times6}{2}+1=16\) 个(按长度 \(5+4+3+2+1=15\) ✓,再加空串)。
求模式串 'ababaab'(位序从 1)的 next 与 nextval 数组。
查看答案
next = 0 1 1 2 3 4 2 (核对 j=7:'ababaa' 头尾只有 'a'='a' 相等,1+1=2 ✓)
nextval = 0 1 0 1 0 4 1 (j=6:a≠t[4]=b → 保留 4;j=7:b=t[2]=b → 取 nextval[2]=1 ✓)
求模式串 'aabaaa'(位序从 1)的 next 与 nextval 数组。
查看答案
next = 0 1 2 1 2 3 (核对 j=3:'aa' 头 'a'=尾 'a',1+1=2 ✓;j=6:'aabaa' 头 'aa'=尾 'aa',2+1=3 ✓)
nextval = 0 0 2 0 0 3 (j=2:a=a → 取 nextval[1]=0;j=3:b≠a → 保留 2;j=6:a≠t[3]=b → 保留 3 ✓)
求模式串 'abcabd'(位序从 1)的 next 与 nextval 数组。
查看答案
next = 0 1 1 1 2 3 (核对 j=6:'abcab' 头 'ab'=尾 'ab',2+1=3 ✓)
nextval = 0 1 1 0 1 3 (j=4:a=a → 取 nextval[1]=0;j=6:d≠t[3]=c → 保留 3 ✓)
主串长 \(n=8\)、模式串长 \(m=3\),BF 算法最坏情况下的比较次数是( )
A. 8 B. 18 C. 24 D. 11
查看答案
B。\((n-m+1)\times m=6\times3=18\) 次(即 4.2 节 S='aaaaaaab'、T='aab' 的逐步核对结果 ✓)。
KMP 算法(含求 next)的时间与空间复杂度为( )
A. \(O(nm)\)、\(O(1)\) B. \(O(m+n)\)、\(O(m)\) C. \(O(m+n)\)、\(O(n)\) D. \(O(n\log_2 m)\)、\(O(m)\)
查看答案
B。求 next \(O(m)\) + 匹配 \(O(n)\);辅助空间是长度 \(m\) 的 next 数组(与主串长无关,C 错)。
下列关于 KMP 算法的叙述中,错误的是( )
A. 匹配过程中主串指针始终不回溯 B. next 值只与模式串自身有关 C. 使用 nextval 可能使比较次数进一步减少 D. KMP 最坏时间复杂度为 \(O(nm)\)
查看答案
D。KMP 最坏也是 \(O(m+n)\)(这正是它相对 BF 的意义);A、B、C 均为 KMP 的基本性质。
块链存储中字符占 1 B、指针占 4 B。要使存储密度不低于 80%,结点大小至少为 \(\underline{\hspace{1.5cm}}\)。
查看答案
设结点大小为 \(k\):\(\frac{k}{k+4}\ge0.8\Rightarrow k\ge16\),即至少 16(验证 \(\frac{16}{16+4}=80\%\) ✓;\(k=12\) 时仅 \(\frac{12}{16}=75\%\) 不达标)。
4.5 本章考点总结
| 考点 | 常考题型 | 热度 | 核心方法 |
|---|---|---|---|
| 串的概念(空串 / 空格串 / 子串 / 相等) | 选择题 | ★★★ | 空格也是字符;子串必须连续;子串数 \(\frac{n(n+1)}{2}+1\) |
| 存储结构(定长 / 堆分配 / 块链) | 选择 / 填空 | ★★ | 定长会截断;存储密度 \(=\frac{\text{串值位}}{\text{实际分配位}}\),结点越大密度越高 |
| BF 最坏比较次数 | 选择 / 填空 | ★★★★ | \((n-m+1)\times m\),'aaa…ab' 型输入触发;失配回溯 \(i=i-j+2\) |
| 手算 next 数组 | 选择 / 解答 | ★★★★★ 几乎必考 | 看 \(t[1..j-1]\) 找最长相等前后缀再 +1;next[1]=0、next[2]=1;next[j]≤j−1 |
| 手算 nextval 数组 | 选择 / 解答 | ★★★★ | 先有 next 再从左修正:\(t[j]=t[\mathrm{next}[j]]\) 则取 nextval[next[j]],否则保留 next[j] |
| KMP 匹配过程(趟数 / 比较次数 / i 不回溯) | 选择 / 解答 | ★★★★ | 失配时 \(j=\mathrm{next}[j]\)、\(i\) 不动,右滑 \(j-\mathrm{next}[j]\) 位;复杂度 \(O(m+n)\)、空间 \(O(m)\) |
| 下标约定(从 1 / 从 0) | 选择(陷阱项) | ★★★ | 408 惯例从 1:0 1 1 2 …;从 0 则整体左移一位,答题前先看题目约定 |