第四章:串⚓︎
约 380 个字 312 行代码 预计阅读时间 6 分钟
本章主要讨论作为一种特殊线性表的“串”(String)数据类型,它在非数值处理领域(如文本编辑、编译器、信息检索等)中应用极为广泛。
串⚓︎
4.1 串的抽象数据类型定义⚓︎
串(String) 是由 n(\(n \ge 0\))个字符组成的有限序列。逻辑结构上,它类似于线性表,但其数据对象被限定为字符集。与线性表的主要区别在于,串的操作通常是针对整个串或子串,而不是单个元素 。
ADT String 的基本操作⚓︎
StrAssign(&T, chars): 赋值操作。将一个字符串常量chars赋给串T。StrCopy(&T, S): 复制操作。由串S复制得到串T。StrEmpty(S): 判空操作。若S为空串"",则返回真 。StrCompare(S, T): 比较操作。按字典序比较串S和T。若S>T,返回值>0;若S<T,返回值<0;若S=T,返回值=0 。StrLength(S): 求长操作。返回串S的元素个数,即串的长度 。Concat(&T, S1, S2): 联接操作。用T返回由S1和S2连接而成的新串 。SubString(&Sub, S, pos, len): 求子串操作。用Sub返回串S从第pos个字符起,长度为len的子串。pos和len的取值必须合法,即 \(1 \le pos \le \text{StrLength}(S)\) 且 \(0 \le len \le \text{StrLength}(S) - pos + 1\) 。Index(S, T, pos): 定位操作(查找子串)。从主串S的第pos个字符起,查找模式串T第一次出现的位置。若找到,返回其在S中的起始位置(位序);否则返回 0 。Replace(&S, T, V): 替换操作。用串V替换主串S中所有与模式串T相等且不重叠的子串 。StrInsert(&S, pos, T): 插入操作。在主串S的第pos个字符前插入串T。StrDelete(&S, pos, len): 删除操作。从主串S中删除从第pos个字符起,长度为len的子串 。ClearString(&S): 清空操作。将S置为空串 。DestroyString(&S): 销毁操作。释放串S占用的存储空间 。
最小操作子集⚓︎
在上述操作中,StrAssign, StrCopy, StrCompare, StrLength, Concat, SubString 构成了串类型的最小操作子集。其他操作(如 Index, Replace 等)都可以基于这个子集来实现 。
-
用最小集实现
Index函数: 其基本思想是,在主串S中从pos位置开始,逐一截取与模式串T等长的子串,并与T进行比较,直到找到匹配或搜索完整个主串 。
4.2 串的表示和实现⚓︎
串的存储结构主要有三种方式。
1. 定长顺序存储表示(静态串)⚓︎
这类似于 Pascal 语言中的字符串,用一个定长的字符数组来存储串,通常数组的第 0 个单元存放串的当前长度 。
| 特性 | Pascal 字符串 (P-string) | C 字符串 (C-string) |
|---|---|---|
| 长度存储 | 第一个字节存储长度 | 以空字符 \0 作为字符串结束标志 |
| 最大长度 | 通常为 255 (如果长度存储在单字节) | 理论上无限制,受限于内存 |
| 字符串读取 | 读取第一个字节即可知道长度,然后按长度读取 | 必须逐个字符读取直到遇到 \0 |
| 获取长度 | O(1) 时间复杂度 (直接读取第一个字节) | O(N) 时间复杂度 (需要遍历整个字符串,N 为长度) |
| 内存使用 | 长度字节 + 实际字符 | 实际字符 + 终止空字符 \0 |
| 字符串操作 | 长度已知,操作 (如拼接) 可能更直接 | 依赖 \0 寻找结束,操作可能需要额外函数 |
| 溢出风险 | 写入超过最大长度时可能覆盖相邻内存 (如果有固定缓冲区) | 写入超过缓冲区时容易发生缓冲区溢出,覆盖 \0 导致更严重问题 |
- 定义:
- 特点:
- 实现简单,操作效率高。
- 长度固定,如果串操作的结果超出了
MAXSTRLEN,会发生截断,丢失一部分数据 。
2. 堆分配存储表示(动态串)⚓︎
这是 C 语言等现代编程语言中常用的方式,串的存储空间在程序运行时动态分配和释放,通常存放在堆(Heap)内存中 。
- 定义:
- 特点:
- 存储空间按需分配,灵活,不会有截断问题 。
- 需要频繁地使用
malloc,realloc,free等函数管理内存,会产生一些额外开销。
- 示例:
Concat操作实现
3. 串的块链存储表示⚓︎
当串非常长时,顺序存储可能无法找到足够大的连续空间。此时可以用链表来存储,每个链表节点存放一个“块”(一个子串),而不是单个字符,以提高存储密度 。
- 定义:
- 应用: 适用于文本编辑器等需要频繁进行插入、删除操作的场景 。
4.3 串的模式匹配算法⚓︎
模式匹配(或子串查找)是串操作中非常核心和重要的一个部分,即Index(S, T, pos)的实现 。
1. 简单(朴素/暴力)匹配算法⚓︎
这是最直观的算法。
- 思想:
从主串
S的pos位置开始,将模式串T与S中等长的子串进行逐一字符比较。- 如果匹配成功,返回起始位置。
- 如果中途失配,主串的比较指针
i回溯到下一个起始位置(即i = i - j + 2,其中j是模式串中失配字符的位置),模式串的指针j回到 1,重新开始新一轮匹配 。
- 时间复杂度: 最坏情况下为 \(O(n \times m)\),其中
n是主串长度,m是模式串长度。
2. KMP 算法 (Knuth-Morris-Pratt)⚓︎
这是一个高效的模式匹配算法,其核心优点是主串的指针 i 不回溯 。
-
思想: 当
S[i]与T[j]发生失配时,简单算法会将i回溯,j置 1。KMP 算法利用已经匹配过的信息(S[i-j+1...i-1] == T[1...j-1]),通过移动模式串T,让它的某个前缀对齐到主串已匹配部分的后缀上,从而避免了i的回溯。模式串应该移动多远,完全由模式串自身的结构决定,这就是next数组的作用 。 -
next数组的定义:next[j] = k的含义是:当模式串的第j个字符T[j]与主串失配时,下一步应该用模式串的第k个字符T[k]去和主串当前字符进行比较 。k的值是模式串T的子串T[1...j-1]中,最长的、相等的“前缀”和“后缀”的长度再加 1。- 规定
next[1] = 0。
- 规定
-
KMP 匹配过程:
C - 时间复杂度: \(O(n+m)\),其中
n是主串长度,m是模式串长度。
- 时间复杂度: \(O(n+m)\),其中
-
next数组的计算: 计算next数组本身也是一个“自己匹配自己”的过程。 -
KMP 算法的改进 (
nextval): 在某些情况下(如T="aaaab"),next数组存在缺陷。当T[j]失配时,如果T[j] == T[next[j]],那么下一次比较也必然失配,这是一次无效的比较。 改进的nextval数组通过在计算时增加一步判断来避免这种情况:如果T[i] == T[j],则nextval[i] = nextval[j],而不是简单地设为j。C
以下是根据PPT内容整理的串(字符串)章节的Markdown文档,包含所有技术细节和代码实现:
第四章 串⚓︎
串的基本概念⚓︎
- 串(String):有限字符序列,由双引号括起,如
"a string" - 空串:长度为0的串,用
""表示 - 子串:串中任意连续字符组成的子序列
- 主串:包含子串的串
- 位置:字符在序列中的序号(从1开始)
- 串相等:长度相等且对应字符相同
串的抽象数据类型(ADT)⚓︎
关键操作说明⚓︎
- StrCompare(S, T):
- S > T → 返回值 > 0
- S = T → 返回值 = 0
- S < T → 返回值 < 0
-
例:
StrCompare("data", "state") < 0 -
SubString(&Sub, S, pos, len):
- 条件:
`1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S)-pos+1- pos就是1开始而非索引
-
例:
SubString(sub, "commander", 4, 3)→ sub = "man" -
Index(S, T, pos):
- 返回T在S中第
`pos字符后第一次出现的位置 - 例:S="abcaabcaaabc", T="bca"
Index(S,T,1)=2,Index(S,T,3)=6,Index(S,T,8)=0
串的存储结构⚓︎
1. 定长顺序存储⚓︎
特点:
- 超过最大长度的串会被截断
- 基本操作基于字符序列复制
串联接示例:
2. 堆分配存储⚓︎
特点: - 动态分配存储空间 - C语言字符串采用此方式(以'\0'结尾)
基本操作实现:
3. 块链存储⚓︎
| C | |
|---|---|
特点:
- 存储密度 = 数据元素所占位 / 实际分配位
- 适用于文本编辑系统(每行作为一个块)
串的模式匹配算法⚓︎
问题定义⚓︎
Index(S, T, pos): 在S中从pos位置开始查找T出现的位置
1. 简单算法(BF)⚓︎
时间复杂度:\(O(n \times m)\)
| C | |
|---|---|
2. 首尾匹配算法⚓︎
改进思路: 1. 先比较模式串首字符 2. 再比较模式串尾字符 3. 最后比较中间字符
3. KMP算法⚓︎
核心思想:利用部分匹配信息,避免主串指针回溯 时间复杂度:\(O(n + m)\)
-
思想: 当
S[i]与T[j]发生失配时,简单算法会将i回溯,j置 1。KMP 算法利用已经匹配过的信息(S[i-j+1...i-1] == T[1...j-1]),通过移动模式串T,让它的某个前缀对齐到主串已匹配部分的后缀上,从而避免了i的回溯。模式串应该移动多远,完全由模式串自身的结构决定,这就是next数组的作用 。 -
next数组的定义:next[j] = k的含义是:当模式串的第j个字符T[j]与主串失配时,下一步应该用模式串的第k个字符T[k]去和主串当前字符进行比较 。k的值是模式串T的子串T[1...j-1]中,最长的、相等的“前缀”和“后缀”的长度再加 1。 - 规定
next[1] = 0。
(1) 算法实现⚓︎
(2) Next函数计算⚓︎
| C | |
|---|---|
(3) Next函数示例⚓︎
| 模式串 | a | b | a | a | b | c | a | c |
|---|---|---|---|---|---|---|---|---|
下标j |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
next[j] |
0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
(4) 改进的Next函数⚓︎
解决特殊情况(如T="aaaab")
| C | |
|---|---|
KMP算法匹配过程示例⚓︎
主串:a c a b a a b a a b c a c a a b c
模式串:a b a a b c
匹配过程:
1. 第一趟:i=2, j=2失败 → j=next[2]=1 i: a c j: a b
2. 第二趟:i=2, j=1失败 → j=next[1]=0 i: c j: a
3. 第三趟:i=8, j=6失败 → j=next[6]=3 i: a b a a b a j: a b a a b c
4. 第四趟:匹配成功 i: a b a(这三个其实未比)a b c j: a b a a b c
串操作应用⚓︎
1. 定位函数Index实现⚓︎
| C | |
|---|---|
2. 替换函数Replace实现⚓︎
本章要点⚓︎
- 熟悉串的7种基本操作定义及实现
- 掌握串的三种存储结构:
- 定长顺序存储
- 堆分配存储
- 块链存储
- 深入理解KMP算法:
- 掌握next函数定义和计算
- 能手工计算next和nextval数组
- 了解串操作的应用特点
作业:4.17, 4.28
补充说明⚓︎
- 存储结构选择:
- 定长顺序存储:适合长度固定的串
- 堆分配存储:适合长度变化的串(C语言标准实现)
-
块链存储:适合文本编辑系统
-
KMP算法核心:
- 部分匹配值:\(\text{PM}[j] = \text{最长公共前后缀长度}\)
- next函数:\(next[j] = \text{PM}[j-1] + 1\)
-
优化nextval:避免相同字符重复比较
-
串操作特点:
- 最小操作子集:赋值、复制、比较、求长、连接、求子串
- 其他操作可通过基本操作实现