知识框架:
4.2 串的定义和实现
字符串简称串,计算机上非数值处理的对象基本都是字符串数据。
常见的信息检索系统 (如搜索引擎 )、文本编辑程序(如 Word 、问答系统、自然语言翻译系统等,都是以字符串数据作为处理对象的。
4.1.1 串的定义
串(string)
是由零格或多个字符组成的有限序列。
一般记为:
S=‘a1a2···an’ (n>=0)
-
S为串名,单引号括起来的字符序列是串的值。
-
ai可以是字母、数字或其他字符。
-
串中字符的个数n称为串的长度。
-
n=0的串称为空串(用 ∅ 表示)
串中任意个连续的字符组成的子序列称为该串的子串
,包含子串的串称为主串
。
某个字符在串中的序号称为该字符在串中的位置。
子串在主串的位置以子串的第一个字符在主串中的位置来表示。
两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等的。
由一个或多个空格(空格为特殊字符)组成的串为空格串,长度为空格字符的个数。
串的操作通常以子串作为操作对象,如查找、插入或删除一个子串等。
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数组值推导
(1) T=“abcdex”
j | 123456 |
---|---|
模式串T | abcdex |
next[j] | 11111 |
1.当j=1时,next[1]=0;
2.当j=2时,j由1到j—1就只有字符“a”,属于其他情况next[2]=1;
3.当j=3时,j由1到j一1串是“ab”,显然“a”与b"不相等,属其他情况,next[3]=1;
4.以后同理,所以最终此T串的next为011111。
(2)T=“abcabx”
j | 123456 |
---|---|
模式串T | abcabx |
next[j] | 011123 |
1.当j=1时,next[1]=0;
2.当j=2时,同上例说明,next[2]=1;
3.当j=3时,同上,next[3]=1;
4.当j=4时,同上,next[4]=1;
5.当j=5时,此时j由1到j一1的串是“abca”,前缀字符“a"与后缀字符“a"相等(前缀用下划线表示,后缀用斜体表示),因此
可推算出k值为2(由'p1...pk-1='pj-k+1…p'j-1,得到p1=p4)因此next[5]=2;
相等,所以next[6]=3。
可以根据经验得到如果前后缀一个字符相等,k值是2,两个字符值是3,n个相等k值就是n+1。
(3) T=“ababaaaba”
j | 123456789 |
---|---|
模式串T | ababaaaba |
next[j] | 011234223 |
1.当j=1时,next[1]=0;
2.当j=2时,同上next[2]=1;
3.当j=3时,同上next[3]=1;
等,next[4]=2;
4.当j=5时,j由1到j一1的串是“abab”,由于前缀字符“ab"与后缀“ab”
相等,所以next[5]=3;
5.当j=6时,j由1到j一1的串是“ababa”,由于前缀字符“aba”与后缀“aba”相等,所以next[6]=4;
"aa”并不相等,只有“a"相等,所以next[7]=2;
6.当j=8时,j由1到j一1的串是“ababaaa”,只有“a"相等,所以next8=2;
7.当j=9时,j由1到j一1的串是“ababaaab”,由于前缀字符“ab"与后缀"ab"相等,所以next[9]=3。
(4) T=“aaaaaaaab”
j | 123456789 |
---|---|
模式串T | aaaaaaaab |
next[j] | 012345678 |
1.当j=1时,next[1]=0;
2.当j=2时,同上next[2]=1;
3.当j=3时,j由1到j一1的串是“aa”,前缀字符“a”与后缀字符“a”相等,next[3]=2;
4.当j=4时,j由1到j一1的串是“aaa",由于前缀字符与后缀“aa"相
等,所以next[4]=3;
5.......
缀“aaaaaa”相等,所以next[9]=8。
4.KMP模式匹配算法实现
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]){/* T[i]表示后缀单个字符,T[j]表示前缀单个字符 */
++i,++j;
next[i]=j; /* 若pi=pj,则next[j+1]=next[j]+1*/
}
else
j=next[j]; /*字符不相同,j值回溯,循环继续 */
}
}
计算当前要匹配的串的T的next数组:
/*返回子串T在主串S中第pos个字符之后的位置,若不存在,则函数返回值为0 */
/*T非空,1<=pos<StrLength(S) */
int INdex_KMP(String S,String T,int pos){
int i=pos; //i用于主串S当前位置下标值,若pos不为1,则从pos位置开始匹配
int j=1; //j用于子串T中当前位置下标值
int next[255];
get_next(T,next);
while(i<=S[0]&&j<=T[0]){
if(j==0||S[i]=T[j]){
++i;
++j;
}
else{
j=next[j];
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
朴素匹配算法时间复杂度为O(mn),KMP算法时间复杂度为O(m+n)
一般情况下,朴素模式匹配实际执行时间近似O(m+n)
KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快的多,主要优点是主串不回溯。
4.2.3 KMP算法进一步优化
KMP 还是有缺陷的。比如,如果我们的主串 S=“aaabcde”,子串T= “aaaaax”, 其 next 数组值分别为 012345。
在开始时,当i=5,j=5时,“b”与“a”不等,如图(1),因此 j=next[5]=4,如图中的(2),此时“b”与第 4 位置的“ a"依然不等,j=next[4]=3,如图中的(3),后依次是(4)(5), 直到j=next[1]时,根据算法,此时 i++、j++, 得到 i=6、j=1,如图中的(6)。
2345步骤,都是多余的判断,T串的第2,3,4,5位置的字符都与首位的“a”相等,可以用首位next[1]的值取代与他相等的字符后续的next[j]的值。
这个取代的数组称为:nextval,代码如下:
//求模式串T的nexy函数修正值并存入nextval
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; //则当前的j为nextval在i的位置的值
else
nextval[i]=nextval[j]; //如果与前缀字符相同,则将前缀字符的nextval //值赋值给nextval在i位置的值
}
else
j=nextval[j]; //字符不相同,j值回溯
}
}
实际匹配算法,只需要将“get_next(T,next);”改成“get_nextval(T,next);”即可。
nextval数值推导
- T=“ababaaaba”
j | 123456789 |
---|---|
模式串T | ababaaaba |
next[j] | 011234223 |
nextval[j] | 010104201 |
1.当i=1时,nextval[1]=0;
2.当j=2时,因第二位字符b”的next值是1,而第一位就是“a”,它们不相等,所以nextval[2]=nex[2]=1,维持原值。
3.当i=3时,因为第三位字符“a"的next值为1,所以与第一位的“a”比较得
知它们相等,所以nextval[3]=nextval[1]=0;如图所示。
- 当j=4时,第四位的字符“b”next 值为 2, 所以与第二位的 "b”相比较得到
结果是相等,因此 nextval[4]=nextval[2]=1; 如图所示。
- 当j=5 时,next 值为 3, 第五个字符“a” 与第三个字符“a”相等,因此nextval[5]=nextval[3]=0;
- 当j=6 时, next 值为 4, 第六个字符“a”与第四个字符“b”不相等,因此nextval [6]=4;
- 当j=7 时,next 值为 2, 第七个字符“a”与第二个字符“b”不相等,因此 nextval[7]=2;
- 当j=8 时, next 值为 2,第八个字符“b" 与第二个字符“b”相等,因此 nextval[8]=nextval[2]=1;
- 当 j=9时, next 值为 3, 第九个字符“a” 与第三个字符 “a”相等,因此 nextval [9]=nextval[3]=1。
- T=“aaaaaaaab”
j | 123456789 |
---|---|
模式串T | aaaaaaaab |
next[j] | 012345678 |
nextval[j] | 000000008 |
- 当 j=1时, nextval[1]=0;
- 当 j=2 时, next 值为 1 , 第二个字符与第一个字符相等,所以 nextval[2]=nextval[1]=0;
- 同样的道理,其后都为 0…;
- 当j=9 时,next 值为 8, 第九个字符“b”与第八个字符“a”不相等,所以nextval[9]=8.
总结:改进过的KMP算法,是在计算出next值的同时,如果a字符与它next值指向的b字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。