数据结构学习笔记——第4章 串
4 串4.1 串的定义和实现4.1.1 串的定义4.1.2 串的存储结构定长顺序存储表示堆分配存储表示块链存储表示
4.1.3 串的基本操作
4.2 串的模式匹配4.2.1 简单的模式匹配算法4.2.2 改进的模式匹配算法——KMP算法字符串的前缀、后缀和部分匹配值KMP算法的原理是什么?
4.2.3 KMP算法的进一步优化
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=′a1a2...an′(n≥0),其中S是串名,单引号括起来的字符序列是串的值;
a
i
a_i
ai可以是字母、数字或其他字符;串中字符的个数n称为串的长度。n=0时的串称为空串(用∅表示)串中任意个连续的字符组成的子序列称为该串的子串,包含子串的串相应地称为主串。某个字符在串中的序号称为该字符在串中的位置。子串在主串中的位置以子串的第一个字符在主串中的位置来表示当两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等的由一个或多个空格(空格是特殊字符)组成的串称为空格串(注意空格串不是空串),其长度为串中空格字符的个数
4.1.2 串的存储结构
定长顺序存储表示
#define MAXLEN 255
typedef struct {
char ch
[MAXLEN
];
int length
;
}SString
;
超过预定义长度的串值会被舍去,称为截断
堆分配存储表示
typedef struct {
char *ch
;
int length
}HString
;
使用malloc()和free()动态存储管理
块链存储表示
利用链表来存放字符串由于串的特殊性,在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符每个结点称为块,整个链表称为块链结构最后一个节点占不满时通常用“#”补上
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)的表
编号12345
SabcacPM00010
下面用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]
Move=(j−1)−PM[j−1]使用部分匹配时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来有些不方便,所以将PM表右移一位,就得到了next数组:
编号12345
SabcacPM-10001
我们注意到:
第一个元素右移以后空缺的用 -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
=
n
e
x
t
[
j
]
+
1
j = j - Move = next[j] + 1
j=j−Move=next[j]+1有时为了使公式更加简洁、计算简单,将 next 数组整体+1。因此上述子串的next数组也可以写成
编号12345
SabcacPM01112
最终得到子串指针变化公式
j
=
n
e
x
t
[
j
]
j = next[j]
j=next[j]在实际匹配过程中,子串在内存里是不会移动的,而是指针在变化next[j] 的含义是:在子串的第 j 个字符与主串发生失配时,则跳到子串的 next[j] 位置重新与主串当前位置进行比较设主串为
′
s
1
s
2
.
.
.
s
n
′
's_1s_2...s_n'
′s1s2...sn′,模式串为
′
p
1
p
2
.
.
.
p
m
′
'p_1p_2...p_m'
′p1p2...pm′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}
next[j]=⎩⎪⎨⎪⎧0,j=1max{k∣1<k<j且′p1...pk−1′=′pj−k+1...pj−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
];
}
}
KMP的匹配算法:
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算法的进一步优化
若是出现 p
j = p
next[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
];
}
}