第四章 串(String)
第四章 串(String)
4.1 串的定义和基本操作
串(String):由零个或多个字符组成的有限序列。记作 ,其中 为串长。 时称为空串(用 表示)。
⚠️ 空串 vs 空格串:空串长度为0(无任何字符),空格串
" "长度为1(包含一个空格字符),二者不同!
子串:串中任意个连续字符组成的子序列。空串是任何串的子串,任何串是自身的子串。
子串位置:子串在主串中第一次出现时第一个字符的位置(位序从1开始)。
串 vs 线性表:
- 串是特殊的线性表——数据对象限定为字符集
- 串的操作对象通常是子串(而非单个元素)——如求子串、模式匹配、串联接等
- 线性表的操作多针对单个元素——如查找、插入、删除某个元素
串的比较:按字典序逐字符比较。字符的大小由其编码值决定(ASCII码、Unicode等)。若所有对应字符相同但长度不同,则较短串更小。
基本操作:
StrAssign(&T, chars):赋值(生成串T)StrCopy(&T, S):复制StrEmpty(S):判空StrLength(S):求串长ClearString(&S):清空DestroyString(&S):销毁Concat(&T, S1, S2):串联接SubString(&Sub, S, pos, len):求子串StrCompare(S, T):串比较Index(S, T, pos):定位(模式匹配)
🗣️ 大白话:串就是一串字符,比如 "hello" 就是一个长度为5的串。"ell" 就是 "hello" 的子串。
串的存储结构
1. 定长顺序存储:用固定长度数组存储,有4种方案记录串长:
| 方案 | 说明 | 特点 |
|---|---|---|
ch[0] 存储串长 | 串内容存在 ch[1] ~ ch[MAXLEN] | 串长受限于 ch[0] 类型(如 char 最大255) |
额外变量 length | 串内容存在 ch[0] ~ ch[length-1] | 常用方案 |
\0 结尾 | 类似C语言字符串 | 求串长需遍历 |
ch[0] 不用 + length | 串内容存在 ch[1] ~ ch[length] | 便于与教材位序从1开始对应 |
2. 堆分配存储:用 malloc 动态分配空间,串长不受限
3. 块链存储:用链表存储,每个结点存多个字符(存储密度 = 实际字符数 / 链表总容量)
⚠️ 块链存储操作不方便、存储密度低,实际中很少使用。
4.2 串的模式匹配
一、朴素模式匹配算法(BF算法)
问题:主串 中查找模式串 的位置。
思路:从主串第1个字符开始,依次与模式串对齐比较,失败就回溯到主串的下一个位置重新比较。
int Index(SString S, SString T) {
int i = 1, j = 1;
while (i <= S.length && j <= T.length) {
if (S.ch[i] == T.ch[j]) {
i++; j++; // 匹配成功,继续比较后续
} else {
i = i - j + 2; // i回溯
j = 1; // j归位
}
}
if (j > T.length) return i - T.length; // 匹配成功
else return 0; // 匹配失败
}
// BF朴素模式匹配(Java下标从0开始)
int indexBF(String S, String T) {
int i = 0, j = 0;
while (i < S.length() && j < T.length()) {
if (S.charAt(i) == T.charAt(j)) {
i++; j++;
} else {
i = i - j + 1; // i回溯
j = 0; // j归位
}
}
if (j >= T.length()) return i - T.length(); // 匹配成功
else return -1; // 匹配失败
}
时间复杂度:
- 最好情况:(第一次就匹配成功, 为模式串长度)
- 最坏情况:( 为主串长度, 为模式串长度)
- 最坏情况示例: = "aaa...aab", = "aab" → 每次前 个字符都匹配,到最后一个才失配
- 实际应用中平均接近 (因为自然语言中连续部分匹配的概率很低)
🗣️ 大白话:BF算法就像笨办法找人——你拿着一张照片从人群第一个人开始一个个比对。如果发现"不是",你就回到下一个人重新开始比对。虽然简单但是慢。
二、KMP算法
核心思想:当发生不匹配时,主串指针 不回溯,只让模式串指针 回退到一个合适的位置。这个"合适的位置"由 next数组 决定。
next数组的含义:next[j] 表示当模式串第 个字符与主串发生不匹配时,模式串需要回退到的位置。也就是模式串 中最长相等真前后缀的长度加1。
next数组的手工求法:
对模式串的每个位置 ,求 的最长相等真前后缀长度,加1。
例:模式串
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| 模式串 | a | b | a | a | b | c | a | c |
| ∅ | "a" | "ab" | "aba" | "abaa" | "abaab" | "abaabc" | "abaabca" | |
| 最长相等前后缀长度 | 0 | 0 | 0 | 1 | 1 | 2 | 0 | 1 |
| next[j] | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
📏 规则:next[1] 固定为 0。
【KMP 匹配过程完整手工模拟】
以主串 = "abaabcac",模式串 = "abaabc" 为例,next = [0, 1, 1, 2, 2, 3]。
| 步骤 | 比较 | 结果 | 动作 | ||
|---|---|---|---|---|---|
| 1 | 1 | 1 | S[1]='a' vs T[1]='a' | ✓ | i++, j++ |
| 2 | 2 | 2 | S[2]='b' vs T[2]='b' | ✓ | i++, j++ |
| 3 | 3 | 3 | S[3]='a' vs T[3]='a' | ✓ | i++, j++ |
| 4 | 4 | 4 | S[4]='a' vs T[4]='a' | ✓ | i++, j++ |
| 5 | 5 | 5 | S[5]='b' vs T[5]='b' | ✓ | i++, j++ |
| 6 | 6 | 6 | S[6]='c' vs T[6]='c' | ✓ | i++, j++, j=7 > T.length=6 → 匹配成功! |
匹配位置 = 。
再举一个需要回退的例子: = "abaacababc", = "ababc",。
| 步骤 | 比较 | 结果 | 动作 | ||
|---|---|---|---|---|---|
| 1 | 1 | 1 | a = a | ✓ | i=2, j=2 |
| 2 | 2 | 2 | b = b | ✓ | i=3, j=3 |
| 3 | 3 | 3 | a = a | ✓ | i=4, j=4 |
| 4 | 4 | 4 | a ≠ b | ✗ | j = next[4] = 2(i 不动!利用已知 "a" 匹配信息) |
| 5 | 4 | 2 | a ≠ b | ✗ | j = next[2] = 1 |
| 6 | 4 | 1 | a = a | ✓ | i=5, j=2 |
| 7 | 5 | 2 | c ≠ b | ✗ | j = next[2] = 1 |
| 8 | 5 | 1 | c ≠ a | ✗ | j = next[1] = 0,故 j==0 → i++, j++ |
| 9 | 6 | 1 | a = a | ✓ | i=7, j=2 |
| 10 | 7 | 2 | b = b | ✓ | i=8, j=3 |
| 11 | 8 | 3 | a = a | ✓ | i=9, j=4 |
| 12 | 9 | 4 | b = b | ✓ | i=10, j=5 |
| 13 | 10 | 5 | c = c | ✓ | j=6 > 5 → 匹配成功!位置 = 10 - 5 = 6 |
💡 手算要诀:
- 先单独求出 next 数组(对模式串 逐位求最长相等真前后缀 + 1)
- 模拟匹配时, 永远只增不减,失配时只动 ()
- 当 时,说明模式串第1个字符就不匹配,此时 和 都加1(开始新一轮比较)
- 考试中常考比较次数:上例中匹配成功共比较了 13 次
⚠️ 2019年真题:KMP 匹配主串 S 和模式串 T,问比较次数。答案是 10 次——关键是准确执行上述步骤表。
KMP算法代码:
int Index_KMP(SString S, SString T, int next[]) {
int i = 1, j = 1;
while (i <= S.length && j <= T.length) {
if (j == 0 || S.ch[i] == T.ch[j]) {
i++; j++;
} else {
j = next[j]; // 模式串向右滑动,i不回溯
}
}
if (j > T.length) return i - T.length;
else return 0;
}
// KMP算法(Java下标从0开始)
// ⚠️ 考研代码陷阱⚠️:Java/C++代码题目中,字符串和next数组下标通常从0开始(next[0]=-1)。
// 而在考研理论选择题/填空题中,默认下标从1开始(next[1]=0)。
// 两者next值恰好差1。务必看清题目要求!
int indexKMP(String S, String T, int[] next) {
int i = 0, j = 0;
while (i < S.length() && j < T.length()) {
if (j == -1 || S.charAt(i) == T.charAt(j)) {
i++; j++;
} else {
j = next[j]; // 模式串向右滑动,i不回溯
}
}
if (j >= T.length()) return i - T.length();
else return -1;
}
// 求next数组(Java版,下标从0开始)
int[] getNext(String T) {
int[] next = new int[T.length()];
next[0] = -1;
int i = 0, j = -1;
while (i < T.length() - 1) {
if (j == -1 || T.charAt(i) == T.charAt(j)) {
i++; j++;
next[i] = j;
} else {
j = next[j];
}
}
return next;
}
时间复杂度:
三、KMP的进一步优化——nextval数组
当 时,回退后仍然会失配,造成不必要的比较。优化后使用 nextval数组:
nextval 手工求法(先求 next,再优化):
例:模式串 = "abaabcac",next = [0, 1, 1, 2, 2, 3, 1, 2]
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|
| T[j] | a | b | a | a | b | c | a | c |
| next[j] | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
| T[next[j]] | — | a | a | b | a | a | a | b |
| T[j]==T[next[j]]? | — | b≠a | a=a | a≠b | b≠a | c≠a | a=a | c≠b |
| nextval[j] | 0 | 1 | 0 | 2 | 1 | 3 | 0 | 2 |
💡 手算规则:逐列检查 是否等于 :
- 若不等:(不优化)
- 若相等:(递归取值,因为回退后一定还会失配)
- 固定为 0
// KMP nextval数组优化(Java版,下标从0开始)
int[] getNextVal(String T) {
int[] next = getNext(T);
int[] nextval = new int[T.length()];
nextval[0] = -1;
for (int j = 1; j < T.length(); j++) {
if (T.charAt(j) == T.charAt(next[j]))
nextval[j] = nextval[next[j]];
else
nextval[j] = next[j];
}
return nextval;
}
🗣️ 大白话:KMP算法的精髓是"别做无用功"。当你发现不匹配时,你已经知道前面一段是匹配的,所以不用从头来过,只需要利用模式串自身的重复结构"滑动"到合适位置继续比较。
📝 真题剖析
【2015年408真题】 已知字符串 为 "abaabaabcabaabc",模式串 为 "abaabc",采用KMP算法进行匹配,第一次出现"失配"()时,();
A. 3, 3 B. 5, 5 C. 6, 6 D. 4, 4
解析:
: a b a a b a a b c a b a a b c : a b a a b c
从 开始逐字符比对:
- : a=a ✓
- : b=b ✓
- : a=a ✓
- : a=a ✓
- : b=b ✓
- : a≠c ✗ → 第一次失配,
答案选C。
📌 第四章总结
| 核心知识点 | 考研要求 |
|---|---|
| 串的定义、子串概念 | 理解概念 |
| 朴素模式匹配(BF算法) | 理解原理和复杂度 |
| KMP算法原理 | 重中之重,必须完全掌握 |
| next数组的手工求法 | 每年必考(选择题或大题) |
| nextval数组的求法 | 高频考点 |
| KMP的时间复杂度 | 牢记 |