本文来源于《数据结构考研复习指导》,仅做笔记记录用。
文章目录
串的定义和实现
字符串简称串,计算机上非数值处理的对象基本都是字符串数据。
串的定义
串(string)是由零个或多个字符组成的有限序列,一般记为
S
=
′
a
1
a
2
.
.
.
a
n
′
(
n
≥
0
)
S='a_1a_2...a_n' (n\geq0)
S=′a1a2...an′(n≥0)
其中:
- S是串名
- 单引号括起来的字符序列是串的值
- ai可以是字母、数字或其他字符
- 串中字符个数n称为串的长度
- n=0时的串称为空串
子串
串中任意连续的字符组成的子序列称为子串,包含子串的串称为主串。某个字符在串中的序号称为该字符在串中的位置。
串相等
当两个串的长度相等,且每个对应位置的字符都相等时,称这两个串是相等的。
空格串
由一个或多个空格(空格是特殊字符)组成的串称为空格串。(空格串不是空串)其长度为串中空格字符从数量。
串的存储结构
定长顺序存储表示
在串的定长顺序存储结构中,为每个串变量分配一个固定长度的存储区,即定长数组。
#define MAXLEN 255 // 预定义最大串长为255
typedef struct {
char ch[MAXLEN]; // 每个分量存储一个字符
int length; // 串的实际长度
} SString;
串的实际长度只能小于等于MAXLEN,超过预定长度的串值会被舍去,称为截断。
串长的表示方式
- 用一个额外的变量len来存放串的长度
- 在串值后面加入一个不计入串长的结束标记字符"\0",此时的串长为隐含值
堆分配存储表示
堆分配存储表示仍然以一组地址连续的存储单元存放串值的字符序列,但它们的存储空间是程序执行过程中动态分配得到的。
typedef struct {
char *ch; // 按串长分配存储区,ch指向串的基地址
int length; // 串的长度
} HString;
在C语言中,存在一个称为“堆”的自由存储区,并用malloc()和free()函数来完成动态存储管理。利用malloc()为每个新产生的串分配一块实际串长需要的存储空间,若分配成功,则返回一个指向起始地点的指针,作为串的基地址,这个串由ch指针来指示;若分配失败,则返回NULL。已分配的空间可以用free()释放。
块链存储表示
类似于线性表的链式存储结构,也可以采用链表方式存储串值。由于串的特殊性(每个元素只有一个字符),再具体实现时,每个结点既可以存放一个字符,也可以存放多个字符。每个结点称为块,整个链表称为块链结构。
串的基本操作
- StrAssign(&T, chars)
赋值操作。把串T赋值为chars。 - StrCopy(&T, S)
复制操作。由串S复制得到串T。 - StrEmpty(S)
判空操作。若S为空串,则返回TRUE,否则返回FLASE。 - 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销毁。
串的模式匹配
子串的定位操作通常称为串的模式匹配,它求的是子串(常称为模式串)在主串中的位置。
简单的模式匹配算法
这里采用定长顺序存储结构,给出一种不依赖其他串的暴力匹配算法。
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; j = 1; // 指针后退重新开始匹配
}
}
if (j > T.length) return i - T.length;
else return 0;
}
时间复杂度为O(nm),其中n和m为主串和模式串的长度。
KMP算法
字符串的前缀、后缀和部分匹配值
前缀
前缀指除最后一个字符串外,字符串的所有头部子串。
后缀
后缀指除第一个字符外,字符串的所有尾部子串。
部分匹配值
部分匹配值为字符串的前缀和后缀的最长相等前后缀长度。
以‘ababa’为例:
子串 | 前缀 | 后缀 | 最长相等前后缀 | 最长相等前后缀长度 |
---|---|---|---|---|
a | 0 | |||
ab | a | b | {a}∩{b}=∅ | 0 |
aba | a,ab | a,ba | {a,ab}∩{a,ba}={a} | 1 |
abab | a,ab,aba | b,ab,bab | {a,ab,aba}∩{b,ab,bab}={ab} | 2 |
ababa | a,ab,aba,abab | a,ba,aba,baba | {a,ab,aba,abab}∩{a,ba,aba,baba}={a,aba} | 3 |
故字符串’ababa’的部分匹配值为00123
例
主串为’ababcabcacbab’,子串为’abcac’
计算子串部分匹配值如下表:
子串 | 前缀 | 后缀 | 最长相等前后缀 | 最长相等前后缀长度 |
---|---|---|---|---|
a | 0 | |||
ab | a | b | 0 | |
abc | a,ab | c,bc | 0 | |
abca | a,ab,abc,abca | a,ca,bca | a | 1 |
abcac | a,ab,abc,abca | c,ac,cac,bcac | 0 |
故子串abcac部分匹配值为00010,将部分匹配值写成数组形式,就得到了部分匹配值(Partial Match, PM)的表。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
PM | 0 | 0 | 0 | 1 | 0 |
使用PM表来进行字符串匹配,如果发生不匹配情况,查表中已匹配的最后一个字符的部分匹配值,按照下面公式算出子串需要向后移动的位数:
移
动
位
数
=
已
匹
配
字
符
数
−
对
应
的
部
分
匹
配
值
移动位数 = 已匹配字符数 - 对应的部分匹配值
移动位数=已匹配字符数−对应的部分匹配值
直到完成匹配。整个匹配过程中,主串始终没有回退,故KMP算法时间复杂度为:O(n+m)
原理
对算法的改进方法为:
已知:右移位数 = 已匹配字符数 - 对应的部分匹配值
写成:Move = (j - 1) - PM[j - 1]
使用部分匹配值时,每当匹配失败,就去找它前一个元素的部分匹配值。改进为将PM表右移以为,这样哪个元素匹配失败,直接看它自己的部分匹配值即可。
将上述字符串’abcac’的PM表右移以为,就得到了next数组
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | -1 | 0 | 0 | 0 | 1 |
其中:
- 第一个元素右移以后的空缺由-1来填充
- 最后一个元素在右移过程中溢出
故上式就可以改写为
M o v e = ( j − 1 ) − n e x t [ j ] Move = (j - 1) - next[j] Move=(j−1)−next[j]
相当于将子串的比较指针j回退到
j = j − M o v e = j − ( ( j − 1 ) − n e x t [ j ] ) = n e x t [ j ] + 1 j = j - Move = j - ((j - 1) - next[j]) = next[j] + 1 j=j−Move=j−((j−1)−next[j])=next[j]+1
有时为了使公式更简洁,将next数组整体+1。
因此,上述next数组也可以写为
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | 0 | 1 | 1 | 1 | 2 |
最终得到的子串指针变化公式为j = next[j]
。next[j]的含义是:在子串的第j个字符与主串发生失配时,则调到子串的next[j]位置重新与主串当前位置进行比较。
next数组一般公式推导
设主串为’s1s2s3…sn’,模式串为’p1p2…pm’,当主串中第i个字符与模式串中第j个字符失配,子串应该向右滑多远,然后与模式中的哪个字符比较?
假设此时应与模式中第k(k < j)个字符继续比较,则模式中前k-1个字符的子串必须满足下列条件,且不可能存在k’>k满足下列条件:
′
p
1
p
2
.
.
.
p
k
−
1
′
=
′
p
j
−
k
+
1
p
j
−
k
+
2
.
.
.
p
j
−
1
′
'p_1p_2...p_{k-1}' = 'p_{j-k+1}p_{j-k+2}...p_{j-1}'
′p1p2...pk−1′=′pj−k+1pj−k+2...pj−1′
(即子串中前有部分子串相同)
- 若存在满足如上条件子串,则发生失配时,仅需将模式向右滑动至模式中第k个字符和主串第i个字符对齐,从模式第k个字符与主串第i个字符继续比较即可。
- 当模式串已匹配相等字符序列中不存在满足上述条件的子串,显然应该将字符串右移j-1位,让主串第i个字符与模式第一个字符进行比较。
- 当模式串第一个字符(j=1)与主串第i个字符失配时,将模式串右移一位,从主串下一位置(i+1)和模式串第一个字符继续比较。
通过上述分析可以得出next函数公式为:
n
e
x
t
[
j
]
=
{
0
j
=
1
m
a
x
{
k
∣
1
<
k
<
j
且
′
p
1
.
.
.
p
k
−
1
′
=
′
p
j
−
k
+
1
.
.
.
p
j
−
1
′
}
当
此
集
合
不
空
时
1
其
他
情
况
next[j] = \begin{cases} 0 \qquad & j = 1\\ max\{k|1 \lt k \lt j 且 'p_1...p_{k-1}'='p_{j-k+1}...p_{j-1}'\} \qquad & 当此集合不空时 \\ 1 \qquad & 其他情况 \end{cases}
next[j]=⎩⎪⎨⎪⎧0max{k∣1<k<j且′p1...pk−1′=′pj−k+1...pj−1′}1j=1当此集合不空时其他情况
求next值的程序性
void get_next(String T, int next[]) {
int i = 1, j = 0;
next[1] = 0;
while (i < T.length) {
if (j == 0 || T.ch[i] == T.ch[j]) {
++i; ++j;
next[i] = j; // 若pi=pj,则next[j+1]=next[j]+1
}
else
j = next[j]; // 否则另j=next[j],循环继续
}
}
KMP算法代码
int Index_KMP(String S, String 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]; // 模式串向右移动
}
if (j > T.length)
return i - T.length; // 匹配成功
else
return 0;
}
KMP算法的进一步优化
假设模式’aaaab’在和主串’aaabaaaab’进行匹配时。
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | a | a | a | b | ||||
j | 1 | 2 | 3 | 4 | 5 | ||||
next[j] | 0 | 1 | 2 | 3 | 4 | ||||
nextval[j] | 0 | 0 | 0 | 0 | 4 |
当i=4、j=4时出现失配,但是如果用之前的next数组则还需要用第2和第3个a重新匹配,这显然不合理。则应将规则修改为:
如果出现上述情况,则需再次递归,将next[j]修正为next[next[j]],直至两者不相等位置,更新后的数组命名为nextval。
计算next数组修正值的算法如下:
void get_nextval(String T, int nextval[]) {
int i = 1, j = 0;
nextval[1] = 0;
while (i < T.length) {
if (j == 0 || T.ch[i] == T.ch[j]) {
++i; ++j;
if (T.ch[i] != T.ch[j]) nextval[i] = j;
else nextval[i] = nextval[j];
}
else
j = nextval[j];
}
}
(白话:
)1
错题整理
- 设有两个串S1和S2,求S2在S1中首次出现的位置的运算称为模式匹配。
关于KMP算法中的nextval【】数组是怎么得到的?https://zhidao.baidu.com/question/572044759.html ↩︎