一、 核心概念与定义
1. 串的定义
串是由零个或多个字符组成的有限序列。记为:`S = 'a₁ a₂ ... aₙ'`(n ≥ 0)。
它是一种特殊的线性表,其数据元素限定为字符。
双(单)引号不是串的一部分,是串的定界符。
2. 关键术语
长度:串中字符的数目 n。
长度为 0 的串称为空串,记为 `''` 或 `∅`。
空格串:由一个或多个空格(' ')组成的串,其长度 ≥ 1。空格串 ≠ 空串。
子串:串中任意个连续的字符组成的子序列。
主串:包含子串的串。
字符位置:字符在序列中的序号(从1开始)。
子串位置:子串在主串中第一次出现时,其首字符在主串中的位置。
串相等:当且仅当两个串的长度相等,并且对应位置的字符也完全相同。
二、 串的存储结构
1. 顺序存储
使用一组地址连续的存储单元存放串的字符序列。
定长顺序存储(静态数组)
cpp
define MAXLEN 255
typedef struct {
char ch[MAXLEN]; // 静态数组
int length; // 串的实际长度
} SString;
缺点:长度固定,操作(如连接、插入)易导致截断或溢出。
堆分配存储(动态数组)
cpp
typedef struct {
char ch; // 按串长动态分配存储区首地址
int length; // 串的长度
} HString;
使用 `malloc`/`free` 动态管理内存,更灵活。
- 链式存储
每个结点可以存放一个或多个字符,用指针连接。
块链存储(常用):
cpp
define CHUNKSIZE 4 // 每个结点存放4个字符
typedef struct Chunk {
char ch[CHUNKSIZE];
struct Chunk next;
} Chunk;
typedef struct {
Chunk head, tail; // 串的头尾指针
int curlen; // 串的当前长度
} LString;
优点:操作灵活,易于插入删除。
缺点:存储密度低(`存储字符数 / 占用总存储空间`),且操作复杂。
存储密度低时,可增大 `CHUNKSIZE`。
对比:顺序存储更常用,因串的操作以整体访问为主,顺序存储效率高、实现简单。
三、 串的基本操作
|-------------------------------|--------------------------------------------------------------|
| 操作名称 | 功能描述 |
| StrAssign(&T, chars) | 生成一个值为字符串chars的串T,即完成对串T的赋值操作。 |
| StrCopy(&T, S) | 将串S中的内容完整地复制到串T中,使得复制后串T与串S完全相同。 |
| StrEmpty(S) | 判断串S是否为空串,若串S中不包含任何字符,则返回真(或特定表示空的标识),否则返回假。 |
| StrCompare(S, T) | 按照字典序对串S和串T进行比较。若S大于T返回正值,S小于T返回负值,两者相等则返回 0。 |
| StrLength(S) | 计算并返回串S中所包含的字符个数,即串S的长度。 |
| SubString(&Sub, S, pos, len) | 从串S的第pos个字符开始,截取长度为len的子串,并将该子串存储在Sub中返回。 |
| Concat(&T, S1, S2) | 将串S1和串S2进行连接操作,形成一个新的串,并用T返回这个连接而成的新串。 |
| Index(S, T, pos) | 在主串S中从第pos个字符之后开始查找与串T相同的子串。若存在,则返回该子串第一次出现的起始位置;若不存在,则返回 0。 |
| Replace(&S, T, V) | 在主串S中查找所有与串T相等且不重叠的子串,并将这些子串全部替换为串V,替换后的结果仍存储在S中。 |
| StrInsert(&S, pos, T) | 在串S的第pos个字符之前插入串T,插入后串S的长度会增加串T的长度。 |
| StrDelete(&S, pos, len) | 从串S的第pos个字符开始,删除长度为len的子串,删除后串S的长度会减少len(若删除范围合法)。 |
| ClearString(&S) | 将串S中的所有字符清空,使其成为一个空串。 |
四、 核心算法:模式匹配
这是串结构的重点和难点,指在主串 S 中查找与模式串 T 完全相同的子串的过程。
1. 朴素模式匹配算法(Brute-Force,BF算法)
思想:从主串的每一个字符开始,依次与模式串的字符进行比较。
过程:
(1) 设指针 `i` 遍历主串 `S`,指针 `j` 遍历模式串 `T`。
(2)若当前字符匹配(`S[i] == T[j]`),则 `i++, j++`。
(3)若不匹配,则发生回溯:`i = i - j + 2`(退回本次匹配的起始位置的下一个字符),`j = 1`,重新开始匹配。
时间复杂度:最坏 `O(nm)`,其中 `n` 为主串长度,`m` 为模式串长度。
优点:简单直观。
2. KMP算法(Knuth-Morris-Pratt)
核心思想:利用已经部分匹配的信息,在发生不匹配时,`i` 指针不回溯,`j` 指针回溯到某个特定位置 `next[j]`,从而大大减少比较次数。
关键概念:
前缀:除最后一个字符外,字符串的所有头部子串。
后缀:除第一个字符外,字符串的所有尾部子串。
部分匹配值(PM) / 最长公共前后缀长度:前缀和后缀的最长共有元素的长度。
next 数组:当模式串的第 `j` 个字符与主串不匹配时,`j` 应跳转到的下一个比较位置。
`next[j] = k` 的含义是:当 `T[j]` 与主串不匹配时,`j` 应跳回 `T[k]` 继续与主串当前字符比较。
求 next 数组(手算步骤):
(1) `next[1] = 0`(固定)。
(2)对于 `j > 1`,`next[j]` = 模式串 `T[1..j-1]` 这个子串的最长公共前后缀长度 + 1。
(3)快速手算技巧:当 `T[j] == T[next[j-1]+1]` 时,`next[j] = next[j-1] + 1`;否则递归向前找 `next[next[j-1]]`。
KMP 算法时间复杂度:`O(n + m)`。
进一步优化:nextval 数组
在 `next` 数组基础上,若 `T[j] == T[next[j]]`,则将 `nextval[j] = nextval[next[j]]`,否则 `nextval[j] = next[j]`。
目的是消除在 `next` 跳转后,仍与主串当前字符相同的无用比较。