串
一些概念
-
串指的是字符串,由零个或多个字符组成的有限序列。当串内没字符的时候称为空串。
-
串中任意多个连续的字符组成的子序列称为该串的子串,包含子串的串称为主串。
-
某个字符在串中的序号称为该字符在串中的位置。
-
子串在主串中的位置以子串的第1个字符在主串中的位置来表示。
-
当两个串的长度相等且每个对应位置的字符都相等时,说明这两个串是相等的。
-
串的基本操作通常以子串为操作对象,如查找、插入、删除一个子串等。
举例子:
有串A = “hello world”,B=“hello”,C=“world”,长度分别为11,5,5;B、C是A的子串,B在A中的位置是1,C在A中的位置是7。
串的存储结构
定长顺序存储表示
用一组地址连续的存储单元来存储串值的字符序列。即用一个固定长度的数组来存放。
#define Maxlen 100 // 预定义最大串长为100
typedef struct {
char ch[Maxlen]; // 每个分量存储一个字符
int length; // 串的实际长度
}SString;
串实际长度小于或等于Maxlen,超过了则会被截断。串长有2种表示方式:
- 用一个额外变量length来存放串的长度,可以用动态分配来防止串长度越界。
- 在串的末尾加入不计入串长的字符串结束符"\0"
堆分配存储表示
用一组地址连续的存储单元来存储串值的字符序列,但是存储空间是在程序执行过程中动态分配得到的。
typedef struct {
char *ch; // 按串长分配存储区,ch指向串的基地址
int length; // 串的长度
}HString;
用malloc() 和 free() 函数来动态存储管理串。这样申请到的用来存储字符串的内存空间是存放在堆区的。
- 堆区(Heap) :是程序运行时用于动态分配内存的区域。通过
malloc、calloc、realloc等函数申请的内存都来自堆区。分配的内存大小和生命周期由程序员控制,分配后必须手动释放(如用free),否则会造成内存泄漏。 - 与之对比的其他内存区域:
- 栈区(Stack) :存储函数的局部变量、函数参数和返回地址。由系统自动分配和释放,生命周期随函数调用结束而结束。
- 全局/静态区(Data Segment) :存储程序中定义的全局变量和静态变量。
- 代码区(Text Segment) :存放程序的机器代码。
块链存储表示
即用链表方式存储串值。具体实现的时候,每个节点既可以存放一个字符,也可以存放多个字符。每个节点称为块,整个链表称为块链结构。当最后一个节点占不满时通常用“#”补上。
串的基本操作
- StrAssign(&T, chars) :赋值操作。把串 T 赋值为 chars。
- StrCopy(&T, S) :复制操作。由串 S 复制得到串 T。
- StrEmpty(S) :判空操作。若 S 为空串,则返回 TRUE;否则返回 FALSE。
- StrCompare(S, T) :比较操作。若 S > T,则返回值 > 0;若 S = T,则返回值 = 0;若 S < T,则返回值 < 0。
- StrLength(S) :求串长。返回串 S 的元素个数。
- SubString(&Sub, S, pos, len) :求子串。用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串。
- Concat(&T, S1, S2) :串联接。用 T 返回由 S1 和 S2 联接而成的新串。
- Index(S, T) :定位操作。若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0。
- ClearString(&S) :清空操作。将 S 清为空串。
- DestroyString(&S) :销毁串。将串 S 销毁。
串的模式匹配
子串的定位操作被称为串的模式匹配,它是求子串在主串中的位置。
简单的模式匹配算法
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
char *S = "hello world"; // 主串
char *T = "world"; // 要查找的子串
int Index(char *Source, char *T)
{
int i = 0, j = 0, t = 0;
// 获取主串和子串的长度
int S_length = strlen(Source);
int T_length = strlen(T);
// 逐字符比较主串 Source 和子串 T;
// 当两个字符匹配时,i 和 j 都向后移动一位,继续比较下一个字符。
// 当遇到不匹配时,重置子串索引 j 为 0,将主串索引 i 调整为当前位置向右移动一位的起始点(按0起始的下标计算)。
// 这里模拟的是朴素匹配算法(暴力匹配),复杂度为 O(mn)
while (i < S_length && j < T_length)
{
t++; // 每次循环都计算一次比较
if (Source[i] == T[j])
{
i++;
j++;
}
else
{
i = i - j + 1; // 回溯到上一次尝试匹配的起始位置的下一个字符
j = 0; // 子串回到开头
}
}
if (j == T_length)
{
printf("i - T_length = %d\r\n", i - T_length);
printf("cnt = %d\r\n", t); // 打印总的比较次数
return i - T_length; // 以0为起点返回位置
}
else
return -1;
}
int main()
{
int retval = Index(S, T);
printf("retval :%d\r\n", retval);
return 0;
}
串的模式匹配算法-KMP算法
概念:
- 前缀(Prefix)
-
字符串的前缀指的是除了整个字符串本身外,从第一个字符开始的所有连续子串。
-
举例来说,对于字符串
"ababa",前缀集合是:"a", "ab", "aba", "abab" -
注意,前缀不包含整个字符串
"ababa"本身。
- 后缀(Suffix)
-
字符串的后缀指的是除了整个字符串本身外,从最后一个字符开始向前所有连续的子串。
-
还是以
"ababa"为例,后缀集合是:"a", "ba", "aba", "baba" -
同样,后缀不包含整个字符串自己
"ababa"。
- 部分匹配值(Partial Match Value)
- 部分匹配值是描述字符串前缀和后缀之间共同的最长相等部分的长度。
- 更具体来说,它表示字符串的某一前缀和对应后缀的最长公共元素的长度(最长相同的字符串部分)。
- 计算部分匹配值时,前缀与后缀必须是“真”的,即:
- 前缀不包含整个字符串的最后一个字符。
- 后缀不包含整个字符串的第一个字符。
- PM表(Partial Match表)
- PM 表的全称是部分匹配表(Partial Match Table)。
- 这是 KMP 算法中用于记录模式串各个位置对应的“最长相等真前缀和真后缀长度”。
- 本质是用来告诉匹配失败时,模式串应该滑动多少位置,避免回退主串指针。
- next数组
- 大多数情况下,next数组就是 PM表的具体实现形态,也即记录最长相等前后缀长度的数组。
- 有部分实现会对 next 数组的起始值做调整,或者用 “next数组 = PM表值向右偏移一位,第一位设为-1” 等方式。
next数组和PM表本质上记录的都是相同信息——模式串每个位置的部分匹配值。
具体例子:主串为ababcabcacbab,子串为abcac
| 子串 | 前缀 | 后缀 | 前缀与后缀交集 | 最长相等前后缀长度 |
|---|---|---|---|---|
| a | {} | {} | ∅ | 0 |
| ab | {a} | {b} | ∅ | 0 |
| abc | {a, ab} | {c, bc} | ∅ | 0 |
| abca | {a, ab, abc} | {a, ca, bca} | {a} | 1 |
| abcac | {a, ab, abc, abca} | {c, ac, cac, bcac} | ∅ | 0 |
-
根据上面交集的最大长度就是部分匹配值。
-
因此,
abcac对应的部分匹配数组(PM表)为:编号 1 2 3 4 5 S a b c a c PM 0 0 0 1 0
求 next 数组
将PM表右移一位,然后将第一个元素右移以后空缺的用-1来填充,因为如果第一个元素就匹配失败了,那必然是要将子串向右移动一位,无需计算移动位数。最后一个元素则溢出,因为原来子串中,最后一个元素的部分匹配值是它的下一个元素使用的,这里没有下一个了所以舍去。
将上面的PM表右移一位,就得到 next 数组为:
| 编号j | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| S | a | b | c | a | c |
| next | -1 | 0 | 0 | 0 | 1 |
已知:右移位数 = 已匹配的字符数 - 对应的部分匹配值
写成:
Move=(j−1)−PM[j−1]
Move = (j - 1) - PM[j - 1]
Move=(j−1)−PM[j−1]
右移一位后得到:
Move=jnew−next[j]
Move = j_{new} - next[j]
Move=jnew−next[j]
Move 的含义是:当子串的第 j 个字符与主串发生失配时,子串右移 Move 位置重新与主串当前位置进行比较。
- 举例
主串: a b a b c a b c a c b a b
模式串:a b c a c
主串指针为i,子串指针为j
计算得到next表为 :
| 编号(j) | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| S | a | b | c | a | c |
| PM[j] | 0 | 0 | 0 | 1 | 0 |
| next[j] | 0 | 1 | 1 | 1 | 2 |
第一次匹配直到失配:
i 等于 2,j 等于 2;在 j 等于 3 的位置失配,失配后i不变仍为2,子串右移大小等于 j - next[j] 即 3 - 1 = 2,所以子串右移2。
第二次匹配直到失配:
i 等于 6,j 等于 4;在 j 等于 5 的位置失配k,失配后i不变仍为6,子串右移大小等于j - next[j] 即 5 - 2 = 3,所以子串右移3。
第三次匹配:
i 等于 10, j 等于 5;在 j 等于 5 的位置完全匹配,匹配结束,返回 j 为子串长度。
改进后的KMP算法
- KMP算法中使用的
next数组:- 传统的失配跳转数组,存储当模式串在某个位置失配时,应该跳转的下标。
- 它的计算直接基于最长相等的前后缀长度。
- 改进后的KMP算法中使用的
nextval数组(或者叫优化版的 next 数组):- 在传统
next数组基础上进一步优化,避免跳转回模式串中重复的字符,减少比较次数。 - 计算时根据
next数组和模式串自身字符做判断,如果S[j] == S[next[j]],则用nextval[next[j]]替代,跳过重复字符。
- 在传统
假设有这么一个模式串:
| j | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| S[j] | a | a | b | a | a | b |
| next[j] | -1 | 0 | 1 | 0 | 1 | 2 |
nextval 数组的定义
- nextval[0] = next[0]
- 对 j > 0:
- 如果 S[j] == S[next[j]],则 nextval[j] = nextval[next[j]]
- 否则 nextval[j] = next[j]
逐项计算:
- j=0
nextval[0] = next[0] = -1 - j=1
S[1] = ‘a’,S[next[1]] = S[0] = ‘a’
所以S[1] 等于 S[next[1]],故 nextval[1] = nextval[next[1]] = nextval[0] = -1 - j=2
S[2] = ‘b’,S[next[2]] = S[1] = ‘a’
所以S[2] 不等于 S[next[2]],故 nextval[2] = next[2] = 1 - j=3
S[3] = ‘a’,S[next[3]] = S[0] = ‘a’,两者相等
故 nextval[3] = nextval[next[3]] = nextval[0] = -1 - j=4
S[4] = ‘a’,S[next[4]] = S[1] = ‘a’,两者相等
故 nextval[4] = nextval[next[4]] = nextval[1] = -1 - j=5
S[5] = ‘b’,S[next[5]] = S[2] = ‘b’,两者相等
故 nextval[5] = nextval[next[5]] = nextval[2] = 1
最终结果:
| j | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| nextval[j] | -1 | -1 | 1 | -1 | -1 |
| j - nextval[j] | 1 | 2 | 1 | 4 | 5 |

被折叠的 条评论
为什么被折叠?



