知识框架:
4.2 串的定义和实现
字符串简称串,计算机上非数值处理的对象基本都是字符串数据。
常见的信息检索系统 (如搜索引擎 )、文本编辑程序(如 Word 、问答系统、自然语言翻译系统等,都是以字符串数据作为处理对象的。
4.1.1 串的定义
串(string)
是由零格或多个字符组成的有限序列。
一般记为:
S=‘a1a2···an’ (n>=0)
-
S为串名,单引号括起来的字符序列是串的值。
-
ai可以是字母、数字或其他字符。
-
串中字符的个数n称为串的长度。
-
n=0的串称为空串(用 ∅ \varnothing ∅ 表示)
串中任意个连续的字符组成的子序列称为该串的子串
,包含子串的串称为主串
。
某个字符在串中的序号称为该字符在串中的位置。
子串在主串的位置以子串的第一个字符在主串中的位置来表示。
两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等的。
由一个或多个空格(空格为特殊字符)组成的串为空格串,长度为空格字符的个数。
串的操作通常以子串作为操作对象,如查找、插入或删除一个子串等。
4.1.2 串的存储结构
- 定长顺序存储表示
类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。
在串的定长顺序存储结构中,为每个串变量分配一个固定长度的存储区,即定长数组
。
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
串的实际长度只能小于等于MAXLEN,超过预定义长度的串值会被舍去,称为截断
。
串有两种表示方法:
- 用一个额外的变量len来存放串的长度。
- 在串值后面加一个不计入串长的结束标记字符“\0”,此时的串长为隐含值。
在一些串的操作(如插入、联接等)中,任意串值序列的长度超过上界MAXLEN,约定用截断
法处理,克服这种弊端,只能不限定串长的最大长度,即采用动态分配的方式。
- 堆分配存储表示
堆分配存储表示仍然是一组地址连续的存储单元存放串值的字符序列,但是他们的存储空间是在程序执行过程中动态分配得到的。
typedef struct{
char *ch;
int length;
}HString;
c语言中有一个称为“堆”的自由存储区,并用malloc()和free()函数来完成动态存储管理。
利用malloc()为每个新产生的串分配一块实际串长所需的存储空间,若分配成功,则返回一个指向其起始地址的指针,作为串的基地址
,这个串由ch指针来指示;
若分配失败,则返回null。已分配的空间可以用free()释放掉。
- 块链存储表示
由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符。
每个结点称为块
,整个链表称为块链结构
。
(a)结点为大小为4的链表(即每个结点存放4个字符),最后一个结点占不满时通常用“#”补上。(b)为结点大小为1的链表。
4.1.3 串的基本操作
StrAssign(&T,chars) | 赋值操作,把串T赋值为chars |
---|---|
StrCopy(&T,S) | 复制操作,由串S复制得到串T |
StrEmpty(S) | 判空操作,若S为空串,则返回True,否则返回False |
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销毁 |
利用判等、求串长和求子串等操作实现定位函数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);
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 简单的模式匹配算法
串的模式匹配:子串的定位操作。
它求的是子串(常称模式串)在主串中的位置。
算法思想:简单的说,就是对主串的每个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符做T的长度的小循环,直到匹配成功或全部遍历完成为止。
//不依赖其他操作的暴力匹配算法
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
retuen 0;
}
最坏时间复杂度O((n-m+1)* m)
最坏时间复杂度为O(mn),其中n和m为主串和模式串的长度。
4.2.2 改进的模式匹配算法-KMP算法
- 字符串的前缀、后缀和部分匹配值
前缀:除最后一个字符以外,字符串的所有头部子串。
后缀:除第一个字符外,字符串的所有尾部子串。
部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。
移动位数=已匹配的字符数-对应的部分匹配值
- KMP算法原理
按照朴素模式匹配算法,应该是23456流程。即主串中当i=2,3,4,5,6时,首字符与子串T的首字符均不等。
观察发现,“abcdex”首字母“a”与后面的串“bcdex”中任意一个字符都不相等,也就是说,子串T的首字符“a”不可能与S串的第2位到第5位字符相等,也就是说2345的判断都是多余的。
如果知道T 串中首字符“a”与 T中后面的字符均不相等。
而 T 串的第一位的“b”与 S串中第二位的“b”在图 (1)中已经判断是相等的,那么也就意味着,T 串中首 字符“a”与 S串中的第二位 "b” 是不需要判断也知道它们是不可能相等了,这样图(2)这一步判断是可以省略的。
同样道理,在知道T 串中首字符“a”与 T 中后面的字符均不相等的前提下,T 串的“a”与 S 串后面的“c”、“d”、"e”也都可以在(1)之后就可以确定是不相等的,所以这个算法当中(2)(3)(4)没有必要,只保留(1)(6)即可。
假设T串后面也含有首字符“a”:
S=“abcabcabc”,T=“abcabx”
同理,2,3,4,5步骤多余。也就是说,对于在子串中有与首字符相等的字符,也是可以省略一部分不必要的判断步骤。
如图,省略掉右图T串的前两位“a”与“b”同S串中的4、5位置字符匹配操作。
在朴素模式匹配算法中,主串的i值是不断回溯来完成的。KMP算法就是为了让没必要的回溯不发生。
i值不回溯,就是不可以变小,考虑的变化就是j值了。T串的首字符与自身后面字符的比较,发现有相等的字符,j值的变化就会不相同。
也就是说,j值的变化与主串没什么关系,关键取决于T串的结构中是否有重复的问题。
T=“abcdex”当中没有重复的字符,j由6变成了1。
T=“abcabx”,前缀“ab”与最后“x”之前串的后缀“ab”是相等的。所以j由6变成了3.
得出规律:j值的多少取决于当前字符之前的串的前后缀的相似度。
把T串各个位置的j值的变化定义为一个next数组,那么next的长度就是T串的长度。得到以下定义函数:
next [ j ] = { 0 , j = 1 max { k ∣ 1 < k < j 且 ′ p 1 ⋯ p k − 1 ′ = ′ p j − k + 1 ⋯ p j − 1 ′ } , 当此集合不空时 1 , 其他情况 \text { next }[j]=\left\{\begin{array}{lc} 0,j=1 \\ \max \left\{k \mid 1<k<j \text { 且 }^{\prime} p_{1} \cdots p_{k-1}{^\prime}={ }^{\prime} p_{j-k+1} \cdots p_{j-1}{ }^{\prime}\right\}, \text { 当此集合不空时 } \\ 1,\text { 其他情况 } \end{array}\right. next [j]=⎩⎨⎧0,j=1max{
k∣1<k<j 且 ′