文章目录
四、串
4.1 串的定义
字符串简称串。
串(String)是由零个或多个字符组成的有限序列,一般记为 S = ′ a 1 a 2 . . . a n ′ ( n ≥ 0 ) S = 'a_1a_2...a_n'\ \ \ (n\geq 0) S=′a1a2...an′ (n≥0) 。
其中,S是串名,单引号括起来的字符序列是串的值; a i a_i ai可以是字母、数字或其他字符,其中字符的个数n称为串的长度。n = 0时的串称为空串( ∅ \emptyset ∅)。
子串:串中任意个连续的字符组成的子序列称为该串的子串。
包含子串的串相应地称为主串。
某个字符在串中的序号称为该字符在串中的位置。子串在主串中的位置以子串的第一个字符在主串中的位置来表示。
两个串的长度相等且每个对应位置的字符都相等时,两个串才是相等的。
空格是特殊字符,由空格组成的串称为空格串。
串的逻辑结构与线性表相似,区别仅在于串的数据对象仅限定为字符集。在基本操作上串和线性表有较大区别。线性表主要以单个元素为操作对象,串的基本操作通常以子串为操作对象。
4.2 串的存储结构
// 预定义最大长度
#define MaxLen 255
typedef struct{
char ch[MaxLen];
int length;
}SString;
// 堆分配表示
typedef struct{
char *ch; // 使用 malloc 和 free 操作
int length;
}HString;
块链表示:使用类似于线性表的链式存储结构,也可采用链表方式存储串值。在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符。每个结点称为块,整个链表称为块链结构。最后一个结点占不满时通常用’#'补上。
4.3 串的基本操作
StrAssignn(&T, chars); //串赋值为chars
StrCopy(&T, S); //S复制得到T
StrEmpty(S); //判S空
StrCompare(S, T); //比较S T大小
StrLength(S); //S长度
SubString(&Sub, S, pos, len); //获得S从pos起长为len的子串
Concat(&T, S1, S2); //T返回链接S1和S2的串
Index(S, T); // 定位操作 若S存在与T相同的子串 返回其在主串中的第一个位置
ClearString(&S); // 清空S
DestroyString(&S); //销毁S
其中, S t r A s s i g n , S t r L e n g t h , S t r C o m p a r e , C o n c a t , S u b S t r i n g StrAssign,\ StrLength,\ StrCompare,\ Concat,\ SubString StrAssign, StrLength, StrCompare, Concat, SubString 5项构成穿类型的最小操作子集,即这些操作不可能用其它串操作实现。
4.4 串的模式匹配
子串的定位操作通常称为串的模式匹配,它求的是子串(模式串)在主串中的位置。(Index)
这里采用定长顺序存储结构。
4.4.1 简单模式匹配算法
最朴素的实现方式,从主串S的第一个字符起,与模式T的第一个字符比较,若相等,则继续逐个比较,否则从主串的下一个字符起,重新和模式的字符比较。以此类推,直到模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功。
时间复杂度为 O ( n m ) O(nm) O(nm) ,m n分别代表模式串和主串的长度。
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 -= (j - 2);
j = 1;
// 重新匹配
}
}
if(j > T.length) return i - T.length;
return 0;
}
4.4.2 改进的模式匹配算法 - KMP
4.4.2.1 改进方向
暴力较坏情况举例: a a a a a a a a a a a a a b aaaaaaaaaaaaab aaaaaaaaaaaaab 与 a a a a a b aaaaab aaaaab
暴力低效的根源:每趟匹配失败都是模式后移一位再从头开始比较。而某趟已匹配相等的字符序列是模式的某个前缀,这种频繁的重复比较相当于模式串在不断地进行自我比较,这就是低效的根源。
如果模式中,已经匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串的指针无需回溯,并继续从该位置开始进行比较。而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。
4.4.2.2 最长相等前后缀
前缀:除最后一个字符以外,字符串的所有头部子串。
后缀:除第一个字符外,字符串所有的尾部子串。
部分匹配值(PM):字符串的前缀和后缀的最长相等前后缀长度。
【例】对于串 " a b a b a " "ababa" "ababa"
前缀: { a , a b , a b a , a b a b } \{a,\ ab,\ aba,\ abab\} {a, ab, aba, abab}
后缀: { a , b a , a b a , b a b a } \{a,\ ba,\ aba,\ baba\} {a, ba, aba, baba}
公共元素: { a , , a b a } \{a,\ ,aba\} {a, ,aba}
部分匹配值: 3 3 3
串 " a b a b a " "ababa" "ababa" 的部分匹配值的表为
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | a | b | a |
PM | 0 | 0 | 1 | 2 | 3 |
00123分别代表串:
a a a,
a b ab ab,
a b a aba aba,
a b a b abab abab,
a b a b a ababa ababa,
的部分匹配值。
得到PM表后进行匹配,如果出现不匹配的情况,那么相应的移动位数的计算方法为
移动位数 = 已匹配的字符数 - 对应的部分匹配值
整个匹配过程,主串不回退。
时间复杂度为 O ( n + m ) O(n+m) O(n+m)。
4.4.2.3 next 数组
移动位数 = 已匹配的字符数 - 对应的部分匹配值
可写成: M o v e = ( j − 1 ) − P M [ j − 1 ] Move=(j-1) - PM[j-1] Move=(j−1)−PM[j−1]
用起来可能会不方便,因此将PM表整体右移一位,得到next数组。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | a | b | a |
next | -1 | 0 | 0 | 1 | 2 |
第一个位置用-1来填充,即 ( j − 1 ) − ( − 1 ) (j-1) - (-1) (j−1)−(−1),若第一个元素匹配失败,需要将子串向右移动一位,而不需要计算子串移动的位数。此时主串指针右移,子串指针不变。
最后一个PM值没有用处,因此可以舍去。
因而改写为: M o v e = ( j − 1 ) − n e x t [ j ] Move = (j-1) - next[j] Move=(j−1)−next[j]
这样 j j j就回退到: j = n e x t [ j ] + 1 j = next[j] + 1 j=next[j]+1
有时为了公式更加简洁、计算更简单,将next数组整体+1。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | a | b | a |
next | 0 | 1 | 1 | 2 | 3 |
此时指针变化为 j = n e x t [ j ] j = next[j] j=next[j]。
next[j]的含义为:在子串的第j个字符发生失配时,则跳转到子串的next[j]位置重新与主串当前位置进行比较。
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] = \left\{ \begin{aligned} &0,&j=1\\ &max\{k|1<k<j\ \&\ 'p_1...p_{k-1}' = 'p_{j-k+1}...p_{j-1}'\}, & 集合非空\\&1,&其他情况 \end{aligned} \right. next[j]=⎩⎪⎨⎪⎧0,max{k∣1<k<j & ′p1...pk−1′=′pj−k+1...pj−1′},1,j=1集合非空其他情况
求解步骤:
- next[1] = 0
- 设next[j] = k。则
- 若 p k = p j p_k = p_j pk=pj,则此时next[j + 1] = k + 1。
- 若 p k ≠ p j p_k\neq p_j pk=pj,则next[j+1] = next[next…[k]] + 1。
p k ≠ p j p_k\neq p_j pk=pj时,即序列 ′ p 1 . . . p k − 1 p k ′ 'p_1...p_{k-1}p_k' ′p1...pk−1pk′ 与序列 ′ p j − k + 1 . . . p j − 1 p j ′ 'p_{j-k+1}...p_{j-1}p_j' ′pj−k+1...pj−1pj′ 在匹配的过程中,在 p k p_k pk位置发生失配。
发生失配意味着要向前移动next[k]个单位继续匹配,如果还不可的话继续移动。
next数组计算函数:
void get_next(SString 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 的情况
} else {
j = next[j]; // pi != pj 的情况 不断循环 直到j降到不再失配为止
}
}
}
4.4.2.5 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];
}
}
if(j > T.length)return i - T.length;
return 0;
}
4.4.2.6 next函数进一步优化 - nextval
原本KMP算法不优的原因。
′ a a a a b ′ 'aaaab' ′aaaab′ 进行匹配时 ′ a a a b a a a a a b ′ 'aaabaaaaab' ′aaabaaaaab′
/*
aaabaaaaab
aaaab!!
aaaab!!
aaaab!!
aaaab!!
aaaab
多余4次
*/
根源在于出现了 p j = p n e x t [ j ] p_j = p_{next[j]} pj=pnext[j]。
因此需要将出现这种情况的next[j]修正为next[next[j]]。
优化后的next数组常更新命名为nextval。
void get_next(SString 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;
// pi == pj 的情况
if(T.ch[i] != T.ch[j]) nextval[i] = j; // 改动
else nextval[i] = nextval[j]; // 改动
} else {
// pi != pj 的情况 不断循环 直到j降到不再失配为止
j = nextval[j];
}
}
}