4 串
4.1 串的定义和实现
4.1.1 串的定义
串 (String)是由零个或多个字符组成的有限序列一般记为
S
=
′
a
1
a
2
.
.
.
a
n
′
(
n
≥
0
)
S='a_1a_2...a_n' (n \geq 0)
S = ′ a 1 a 2 . . . a n ′ ( n ≥ 0 ) ,其中S是串名 ,单引号括起来的字符序列是串的值 ;
a
i
a_i
a i 可以是字母、数字或其他字符 ;串中字符的个数n称为串的长度 。n=0时的串称为空串 (用∅表示) 串中任意个连续的字符组成的子序列称为该串的子串 ,包含子串的串相应地称为主串 。某个字符在串中的序号称为该字符在串中的位置 。子串在主串中的位置以子串的第一个字符在主串中的位置来表示 当两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等 的 由一个或多个空格(空格是特殊字符)组成的串称为空格串 (注意空格串不是空串),其长度为串中空格字符的个数
4.1.2 串的存储结构
定长顺序存储表示
#define MAXLEN 255
typedef struct {
char ch[ MAXLEN] ;
int length;
} SString;
堆分配存储表示
typedef struct {
char * ch;
int length
} HString;
块链存储表示
利用链表来存放字符串 由于串的特殊性,在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符 每个结点称为块 ,整个链表称为块链结构 最后一个节点占不满时通常用“#”补上
4.1.3 串的基本操作
StrAssign(&T, chars) :赋值操作。把串T赋值为charsStrCompare(S, T) :比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0StrLength(S) :求串长。返回串S的元素个数SubSrting(&Sub, S, pos, len) :求子串。用Sub返回串S的第pos个字符起长度为len的子串Concat(&T, S1, S2) :串联接。用T返回由S1和S2联接而成的新串以上五种操作构成串类型的最小操作子集 StrCopy(&T, S) :复制操作。由串S复制得到串TStrEmpty(S) :判空操作。若S为空串,则返回TRUE,否则返回FALSEIndex(S, T) :定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置;否则函数值为0Replace(&S, T, V) :替换子串操作。用V替换主串S中出现的所有与T相等的不重叠的子串StrInsert(&S, pos, T) :插入操作。在串S的第pos个字符之前插入串TStrDelete(&S, pos, len) :删除子串。从串S中删除第pos个字符起长度为len的子串ClearString(&S) :清空操作。将S清为空串DestroyString(&S) :销毁串。将串S销毁例如,可用判等、求串长和求子串等操作实现定位函数Index(S, T)。算法的思想为:在主串S中取从第一个字符起、长度和串T相等的子串,与串T比较,若相等则求得函数值为i,否则i值增1,直至串S中不存在和串T相等的子串为止
int Index ( String S, String T) {
int i = 1 , n = StrLength ( S) , m = StrLength ( T) ;
String sub;
while ( i <= n- m+ 1 ) {
SubString ( sub, S, i, m) ;
if ( StrCompare ( sub, T) != 0 )
i++ ;
else
return i;
}
return 0 ;
}
4.2 串的模式匹配
4.2.1 简单的模式匹配算法
子串的定位操作通常称为串的模式匹配 它求的是子串(常称模式串 )在主串中的位置 这里采用定长顺序存储结构,给出一种不依赖于其他串操作的暴力匹配算法
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分别主串和模式串的长度
4.2.2 改进的模式匹配算法——KMP算法
字符串的前缀、后缀和部分匹配值
前缀 指除最后一个字符以外,字符串的所有头部子串;后缀 指除第一个字符外,字符串的所有尾部子串;部分匹配值 则为字符串的前缀和后缀的最长相等前后缀长度下面以主串为 a b a b c a b c a c b a b,子串为 a b c a c为例:
‘a’ 的前缀和后缀都为空集,最长相等前后缀长度为0 ‘ab’ 的前缀为{a},后缀为{b},{a} ∩ {b} = ∅,最长相等前后缀长度为0 ‘abc’ 的前缀为{a, ab},后缀为{c, bc},{a, ab} ∩ {c, bc} = ∅, 最长相等前后缀长度为0 ‘abca’ 的前缀为{a, ab, abc},后缀为{a, cx, bca},{a, ab, abc} ∩ {a, cx, bca} = {a},最长相等前后缀长度为1 ‘abcac’ 的前缀为{a,ab, abc, abca},后缀为{c, ac, cac, bcac},{a,ab, abc, abca} ∩ {c, ac, cac, bcac} = ∅,最长相等前后缀长度为0 故字符串 ‘abcac’ 的的部分匹配值为 0 0 0 1 0。将部分匹配值写成数组形式,就得到了部分匹配值(Partial Match,PM)的表
下面用PM表来进行字符串匹配: 第一趟匹配过程:
发现 c 与 a 不匹配,前面的2个字符 ‘ab’ 是匹配的,查表可知,最后一个匹配字符 b 对应的部分匹配值为0,因此按照下面的公式算出子串需要向后移动的位数:
移
动
位
数
=
已
匹
配
的
字
符
数
−
对
应
的
部
分
匹
配
值
移动位数 = 已匹配的字符数 - 对应的部分匹配值
移 动 位 数 = 已 匹 配 的 字 符 数 − 对 应 的 部 分 匹 配 值 因为 2 - 0 = 2,所以将子串向后移动2位,如下进行第二趟匹配: 第二趟匹配过程:
发现 c 与 b 不匹配,前面4个字符 ‘abca’ 是匹配的,最后一个匹配字符对应的部分匹配值为1 4 - 1 = 3,将子串向后移动3位,如下进行第三趟匹配: 第三趟匹配过程:
整个匹配过程中,主串始终没有回退,故KMP算法可以在O(n+m)的时间数量级上完成串的模式匹配操作,大大提高了匹配效率
KMP算法的原理是什么?
对算法的改进方法:
已知:
右
移
位
数
=
已
匹
配
的
字
符
数
−
对
应
的
部
分
匹
配
值
右移位数 = 已匹配的字符数 - 对应的部分匹配值
右 移 位 数 = 已 匹 配 的 字 符 数 − 对 应 的 部 分 匹 配 值 写成:
M
o
v
e
=
(
j
−
1
)
−
P
M
[
j
−
1
]
Move = (j - 1) - PM[j - 1]
M o v e = ( j − 1 ) − P M [ j − 1 ] 使用部分匹配时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来有些不方便,所以将PM表右移一位,就得到了next数组:
我们注意到:
第一个元素右移以后空缺的用 -1 来填充,因为若是第一个元素匹配失败,则需要将子串向右移动一位,俄日不需要计算子串移动的位数 最后一个元素在右移的过程中溢出,因为原来的子串中,最后一个元素的部分匹配值是其下一个元素使用的,但显然已没有下一个元素 ,故可以舍去 这样,上式就改写为
M
o
v
e
=
(
j
−
1
)
−
n
e
x
t
[
j
]
Move = (j - 1) - next[j]
M o v e = ( j − 1 ) − n e x t [ j ] 相当于将子串的比较指针 j 回退到
j
=
j
−
M
o
v
e
=
n
e
x
t
[
j
]
+
1
j = j - Move = next[j] + 1
j = j − M o v e = n e x t [ j ] + 1 有时为了使公式更加简洁、计算简单,将 next 数组整体+1。因此上述子串的next数组也可以写成
最终得到子串指针变化公式
j
=
n
e
x
t
[
j
]
j = next[j]
j = n e x t [ j ] 在实际匹配过程中,子串在内存里是不会移动的,而是指针在变化 next[j] 的含义是:在子串的第 j 个字符与主串发生失配时,则跳到子串的 next[j] 位置重新与主串当前位置进行比较 设主串为
′
s
1
s
2
.
.
.
s
n
′
's_1s_2...s_n'
′ s 1 s 2 . . . s n ′ ,模式串为
′
p
1
p
2
.
.
.
p
m
′
'p_1p_2...p_m'
′ p 1 p 2 . . . p m ′ 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\left [ j \right ] = \begin{cases}0, j=1\\ max\left \{k|1<k<j 且 'p_1...p_{k-1}' = 'p_{j-k+1}...p_{j-1}'\right \}, 当此集合不空时 \\1, 其他情况\end{cases}
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 值的程序如下:
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;
}
else
j = next[ j] ;
}
}
int Indecx_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 ;
}
在一般情况下,普通模式匹配的实际执行时间近似为O(m+n),因此至今仍被采用。KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点是主串不回溯
4.2.3 KMP算法的进一步优化
若是出现 pj = pnext[j] 则后续匹配必然失配,因此应当避免。如何处理? 如果出现了,则需要再次递归,将 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] ;
}
}