串的相关术语
串也是一种特殊的线性表。栈和队列是一种操作受限的线性表,而这里的串是一种内容受限的线性表,规定串里面的内容只能是字符。
-
串:零个或多个任意字符组成的有限序列,例如,
s ⎵ ( 串 名 ) = " a 1 a 2 ⋯ a n ⎵ ( 串 值 ) ” ( n ⎵ ( 串 长 ) ≥ 0 ) \underbrace {\rm{s}}_{(串名)} = " \underbrace {{a_1}{a_2} \cdots {a_n}}_{(串值)}\ ” (\underbrace n_{(串长)} \ge 0) (串名) s="(串值) a1a2⋯an ”((串长) n≥0)
空串:n=0,空串用 ϕ \phi ϕ表示。 -
子串:串中任意个连续字符组成的子序列(含空串)称为该串的子串,例如,
“ a b c d e ” 的 子 串 有 : “ ” 、 “ a ” 、 “ a b ” 、 “ a b c ” 、 “ a b c d ” 和 “ a b c d e ” 等 “abcde”的子串有:“”、“a”、“ab”、“abc”、“abcd”和“abcde”等 “abcde”的子串有:“”、“a”、“ab”、“abc”、“abcd”和“abcde”等
真字串:不包含自身的所有子串。 -
主串:包含子串的串相应的称为主串。
-
字符位置:字符在序列中的序号为该字符在串中的位置。
-
子串位置:子串第一个字符在主串中的位置。
-
空格串:由一个或多个空格组成的串,与空串不同。
-
串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,这两个串才是相等的。所有空串都是相等的。
例如,字符串a、b、c、d
a=‘BEI’
b=‘JING’
c=‘BEIJING’
d=‘BEI JING’
则,
它们的长度是:3 4 7 8
c的子串有:a b
d的子串有:a b
a在c中的位置:1(从1开始计数)
a在d中的位置:1
b在c中的位置:4
b在d中的位置:5
串的类型定义和存储结构
串的抽象类型定义为;
ADT String
{
数据对象:
D
=
{
a
i
∣
a
i
∈
C
h
a
r
a
c
t
e
r
S
e
t
,
i
=
1
,
2
,
.
.
.
,
n
,
n
≥
0
}
D=\{a_i|a_i \in CharacterSet,i=1,2,...,n,n\geq 0 \}
D={ai∣ai∈CharacterSet,i=1,2,...,n,n≥0}
数据关系:
R
=
{
<
a
i
−
1
,
a
i
>
∣
a
i
−
1
,
a
i
∈
D
,
i
=
1
,
2
,
.
.
.
,
n
}
R=\{<a_{i-1},a_i>|a_{i-1},a_i \in D,i=1,2,...,n\}
R={<ai−1,ai>∣ai−1,ai∈D,i=1,2,...,n}
基本操作: 有串赋值,串比较等操作。
}ADT String
串的存储结构
- 串的顺序存储结构:顺序串
其类型定义如下:
#define MAXLEN 255 //串的最大长度
typedef struct
{
char ch[MAXLEN + 1]; //存储串的一维数组,下标从0带到255
int length; //串的当前长度
}SString;
后面为了方便起见,一般字符数组的0号位闲置不用,从1号位开始存储,即下标从1开始计数。
- 串的链式存储结构:链串
按照常规单链表的方式,其结构如下图,
这种链串结构有很明显的缺点:存储密度低。
说明一下,一个字符在内存中占1个字节,而指针域占4个字节,所以其存储密度只有0.2。
在链串中,可以将多个字符放在一个结点中,以克服其存储密度低的缺点——块链结构,如下图所示,
这里的块的大小可由用户定义。
块链结构的类型定义如下:
#define CHUNKSIZE 80 //块的大小可由用户定义
typedef struct Chunk
{
char ch[CHUNKSIZE];
strcat Chunk* next;
}Chunk;
typedef struct
{
Chunk *head, *tail; //串的头指针和尾指针
int curlen; //串当前的长度
}LString; //字符串的块链结构
串的匹配算法
算法目的:确定主串中所含子串第一次出现的位置(定位)。
算法种类:
- BF算法(Brute-Force)
- KMP算法(特点:速度快)
BF算法
BF算法((Brute-Force)又称作简单匹配算法,采用穷举法的思路。
下面举例说明: 现有一主串(正文串)S和一子串(模式串)T,
S
:
a
a
a
a
a
b
,
n
=
6
T
:
a
a
a
b
,
m
=
4
S: aaaaab ,n=6 \\ T:aaab,m=4
S:aaaaab,n=6T:aaab,m=4
算法思路是将S中的每一个字符开始依次与T中的字符进行匹配。
其匹配过程如下:
1.
S
:
a
↓
i
  
a
  
a
  
a
⎵
(
匹
配
字
符
)
  
a
  
b
T
:
a
↑
j
  
a
  
a
  
b
\begin{array}{l} S:\underbrace {\mathop a\limits^{ \downarrow i} \;a\;a\;a}_{(匹配字符)}\;a\;b\\ \\ T:\mathop a\limits_{ \uparrow j} \;a\;a\;b \end{array}
S:(匹配字符)
a↓iaaaabT:↑jaaab
i从1开始,往后m个字符逐个与T中的字符相匹配。
2.若i=1匹配失败,则把i的初始位置往后移一位,i=2开始,如下图
S
:
a
  
a
↓
i
  
a
  
a
  
a
⎵
(
匹
配
字
符
)
  
b
T
:
a
↑
j
  
a
  
a
  
b
\begin{array}{l} S:a\;\underbrace {\mathop a\limits^{ \downarrow i} \;a\;a\;a}_{(匹配字符)}\;b\\ \\ T:\mathop a\limits_{ \uparrow j} \;a\;a\;b \end{array}
S:a(匹配字符)
a↓iaaabT:↑jaaab
当i=1匹配失败时,此时i指向的是第4个字符,j指向T中第4个字符的,此时怎么让i回到2,这里采用回溯的方法,i和j的值为:
i=i-j+2; //回溯
j=1; //从头开始
3.从i=2开始也匹配失败,继续回溯,
S : a    a    a ↓ i    a    a    b ⎵ ( 匹 配 字 符 ) T : a ↑ j    a    a    b \begin{array}{l} S:a\;a\;\underbrace {\mathop a\limits^{ \downarrow i} \;a\;a\;b}_{(匹配字符)}\\ \\ T:\mathop a\limits_{ \uparrow j} \;a\;a\;b \end{array} S:aa(匹配字符) a↓iaabT:↑jaaab
此时,可以看到当从i=3开始时,匹配成功,此时的 i=7.j=5,返回值为i-t.length=3
。
这个算法的思路如下:
Index(S,T,pos),这里的pos表示从主串的第pos个字符开始比较
- 将主串的第pos个字符和模式串的第一个字符比较;
- 若相等,继续逐个比较后续字符;
- 若不等,从主串的下一个字符起(回溯),重新与模式串的第一个字符比较。
- 直到主串的一个连续子串字符序列与模式串相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。
- 否则,匹配失败,返回0。
算法描述:
int Index_BF(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; //模式匹配不成功
}
BF算法的时间复杂度:
若n为主串的长度,m为子串的长度,
- 最好的情况是,不用回溯,则一共比较了m次,时间复杂度为 O ( m ) O(m) O(m)。
- 最坏的情况是,主串前面的n-m个位置都部分匹配到子串的最后一位,即,这n-m位比较了m次,另外随后m位也各比较了一次,
总 次 数 为 : ( n − m ) ∗ m + m = ( n − m + 1 ) ∗ m 总次数为:(n-m)*m+m=(n-m+1)*m 总次数为:(n−m)∗m+m=(n−m+1)∗m
若 m < < n m<<n m<<n,则该算法复杂度为 O ( n ∗ m ) O(n*m) O(n∗m)。
总的平均复杂度为 O ( n ∗ m ) O(n*m) O(n∗m)。
KMP算法
KMP算法由D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的,简称KMP算法。
与BF算法相比,它的运算效率更高,主要体现在两个方面:
- 主串S的指针i不必回溯;
- 它利用已经部分匹配的结果从而加快模式串的滑动速度,即子串的指针j不用每次都从头开始比较。
针对第一点,如下图所示,
当匹配失败时,指针i不用回溯到2,而是从匹配失败的位置开始,即上图中i=3的字符a开始,再往后比较。
针对第二点,如下图所示,
上图中,T里的第1个和第4个字符相同,都是a,此时的j就从第2个元素开始(因为此时第1个元素a已经匹配好了)逐个向后比较;
那么,对于j的下一个位置,这里可以用一个数组next[j]来存储。
next[j]数组
next数组表明模式串中第j个字符与主串中相应字符“失配”时,在模式串中需重新和主串中该字符进行比较的字符的位置。
其计算方法如下,
n e x t [ j ] = { max { k ∣ 1 < k < j , p 1 ⋯ p k − 1 ⏞ ( 从 头 开 始 的 k − 1 个 元 素 ) = p j − k + 1 ⋯ p j − 1 ⏞ ( j 前 面 的 k − 1 个 元 素 ) } 0                        当 j = 1 时 1                        其 他 情 况 next[j] = \left\{ {\begin{array}{l} {\max \{ k|1 < k < j,\overbrace {{p_1} \cdots {p_{k - 1}}}^{(从头开始的k-1个元素)} = \overbrace {{p_{j - k + 1}} \cdots {p_{j - 1}}}^{(j前面的k-1个元素)}\} }\\ {0\;\;\;\;\;\;\;\;\;\;\;当j = 1时}\\ {1\;\;\;\;\;\;\;\;\;\;\;其他情况} \end{array}} \right. next[j]=⎩⎪⎪⎨⎪⎪⎧max{k∣1<k<j,p1⋯pk−1 (从头开始的k−1个元素)=pj−k+1⋯pj−1 (j前面的k−1个元素)}0当j=1时1其他情况
例子如下:
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串 | a | b | c | a | a | b | b | c | a | b | c | a | a | b | d | a | b |
next[j] | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1 | 2 |
KMP算法描述:
int Index_KMP(SString S, SString T, int pos)
{
int i = pos, j = 1;
while (i <= S.length&&j <= T.length)
{
if (S.ch[i] == T.ch[j])
{
++i;
++j;
}
else
j = next[j]; //i不变,j后退
}
if (j >= T.length)
return i - T.length; //返回匹配的第一个字符的下标
else
return 0; //模式匹配不成功
}
next[j]数组的求法描述:
void get_next(SString T, int& next[])
{
i = 1;
next[1] = 0;
j = 0;
while (i < T.length)
{
if (j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
next[j] = j;
}
else
j = next[j];
}
}
next[j]数组的改进
对于某些特殊情况,如下图,
其next数组为,
但在这里,因为模式串p中由好几个元素一样,所以j指针的位置如果继续用next数组里的值,就会不合适,这里对next函数进行一个修正,修正过的值存在nextval数组里,修正方法如下图,
nextval数组的求法描述为:
void get_nextval(SString T, int& nextval[])
{
i = 1;
nextval[1] = 0;
j = 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];
}
}
这里,KMP算法的复杂度为 O ( n + m ) O(n+m) O(n+m)。