数据结构笔记
四、串
1. 串的定义和实现
1.1 串的定义
- 串(String):由0个或者多个字符组成的有限序列。一般记为:S=‘a1a2…an’ (n>=0)
- 串名:S
- 串值:引号内部内容
- 串长:串内字符个数
- 空串:串长等于0的串
- 子串:串中任意个连续的字符组成的子序列
- 主串:包含主串的串
- 字符在主串中的位置:字符在串中第一次出现的位置
- 子串在主串中的位置:子串的第一个字符在主串中的位置
- 空格串:由空格组成的串,每个空格占1B
串数据对象限定为字符集(中文字符、英文字符、数字字符、标点字符等等)
1.2 串的存储结构
1. 定长顺序存储表示
存储结构:
- 一组地址连续的存储单元存储串值的字符序列,即使用定长数组存储。
- ch[MAXLEN]:每个分量存储一个字符。
- int length:串的实际长度。
typedef struct {
char ch[MAXLEN];
int length;
}SString;
2. 堆分配存储表示
存储结构:
- 一组地址连续的存储单元存储串值的字符序列,但是存储空间是动态分配得到的(在堆区)。
- char ch:按照串长分配存储区域,ch指向串的基地址。
- int length:串的实际长度。
typedef struct {
char *ch;
int length;
}HString;
顺序串的实现细节:
-
方案一:使用int length的方法存储长度
特点:占用额外的空间,但是清楚明了,能够表示的串长范围大。 -
方案二:使用ch[0]存储length
特点:字符位序与数组下标相同,但是ch[0]只能存储1B数据,所以串长最多为256. -
方案三:没有length变量,以 /0 表示结尾
特点:无法直接的表示串长。 -
方案四:不适用char[0],再使用length存储串长
特点:直观表示串长,并且数组下标与字符位序相同。
3. 块链存储表示
存储结构:
- 链式存储
- 节点可以存放多个字符增大存储密度
- 最后一个结点不满时用#补上
typedef struct StringNode{
char ch[4];
struct StringNode * next;
}StringNode,*String;
1.3 串的基本操作
串的基本操作
- StrAssign(&T,chars):赋值操作。把串T赋值为chars。
- StrCopy(&T,S):复制操作,由串S复制得到串T。
- StrEmpty(S):判窄操作。若s为空串,则返回TRUE,否则返回FALSE。
- StrLength(S):求串长。返回串s的元索个数·
- Clearstring(&S):清空操作。将s清为空串,
- DestroyString(&S):销毁串。将串s销毁(回收存储空间)。
- Concat(&T,S1,S2):串联接。用T返回由SI和S2联接而成的新串。
- SubString(&Sub,S,pos,len):求子串。用sub返回串s的第pos个字符起长度为len的子串。
- lndex(S,T):定位操作。若主串s中存在与串T值相同的子串,则返回它在主串s中第一次出现的 位置:否则函数值为0·
- StrCompare(S,T):比较操作。若S>T,则返回值>0:若S=T,则返回值=0:若S<T,则返回值<0。
1. SubString(&Sub,S,pos,len)
bool SubString (SString &Sub,SString S,int pos,int len){
if (pos+len-1>S.length)
return false;
for (int i=pos;i<pos+len;i++){
Sub.ch[i-pos+1]=S.ch[i];
}
Sub.length=len;
return true;
}
2. StrCompare(S,T)
int StrCompare(SString S,SString T){
for (int i=1;i<=S.length && i<=T.length;i++){
if (S.ch[i]!=T.ch[i])
return S.ch[i]-T.ch[i];
}
return S.length-T.length;
}
3. lndex(S,T)
int Index(SString S,SString T){
int i=1,n=S.length,m=T.length;
SString sub;
while (i<=n-m+1){
SubString(sub,S,i,m);
if (StrCompare(sub,T)!=0)
i++;
else return i;
}
return 0;
}
1.2 串的模式匹配
1. 朴素模式匹配算法
- 串的匹配模式:在主串中找到与模式串相同的子串,并返回其位置。
算法思想与步骤:
- 使用i、j分别指示主串S和模式串T 待比较字符的位置。
- 从主串的S的第一个字符起,与模式串T的第一个字符比较,若相等则逐个比较后续字符,否则从主串的下个字符起重新与模式串字符比较。
- 直至模式串T中的每个字符依次和主串S中的每一个连续的字符序列相等,则匹配成功,返回值为与模式串T第一个字符相等的字符在主串的序号,否则匹配不成功,返回0。
int StringIndex(SString S,SString T){
int k=1;
int i=k,j=1;
while (i<=S.length&&j<=T.length){
if(S.ch[i]==T.ch[j]){
++i;
++j;
}else{
k++;
i=k;
j=1;
}
}
if (j>T.length)
return k;
else
return 0;
}
性能分析
较好的情况:每个子串的第一个字符就与模式串不匹配。
若模式串的长度为m,主串长为n,则:
-
匹配成功的最好时间复杂度:O(m)
-
匹配失败的最好时间复杂度:O(n-m+1)=O(n-m)=O(n)
-
匹配成功的最坏时间复杂度:O[(n-m+1)*m]=O(mn)
-
匹配失败的最坏时间复杂度:O[(n-m+1)*m]=O(mn)
2. 模式匹配算法-KMP
朴素算法缺点:匹配失败后模式串后移一位从头开始比较。某趟已匹配的相等的字符序列是模式的某个前缀,频繁的重复比较相当于模式串进行自我比较。
算法思想与步骤:
- 使用i、j分别指示主串S和模式串T 待比较字符的位置。
- 从主串的S的第一个字符起,与模式串T的第一个字符比较,若相等则逐个比较后续字符,否则将跳转到子串的next[i]位置重新与主串的当前位置进行比较。
- 直至模式串T中的每个字符依次和主串S中的每一个连续的字符序列相等,则匹配成功,返回值为与模式串T第一个字符相等的字符在主串的序号,否则匹配不成功,返回0。
2.1 由PM到next数组
- 前缀:除了最后一个字符以外,字符串的所有子串。
- 后缀:除了第一个字符以外,字符串的所有尾部子串。
- 部分匹配值(Partial Match):前缀和后缀的最长相等前后缀长度值。
可知abcac的部分匹配值(PM)为00010,写为数组形式:
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
PM | 0 | 0 | 0 | 1 | 0 |
所以可知,当在第j位匹配失败时,子串的移动位数move:
move=已匹配字符数-对应部分的匹配值=j-1-PM[j-1]
利用数学推导从PM数组到next数组:
- move=j-1-PM[j-1]
- next[i]=PM[i]-1 (PM数组整体右移,使得能够使用到不匹配的字符,而不是它的前一个字符)
- move=(j-1)-next[j]
- j=j-move=j-((j-1)-next[j])=next[j]+1
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | 0 | 1 | 1 | 1 | 2 |
next[j]的含义,在子串的第j个字符与主串发生失配时,则跳到子串的next[j]的位置重新与主串的当前位置进行比较
2.2 next数组的计算
求法:当第j个字符匹配失败时,由1-j-1个字符组成的串记为S,则next[j]=S的最长相等前后缀长度+1。特别地next[1]=0。
函数表达式为:
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[j]= \begin{cases} 0& \text{j=1}\\ Max {k|1<k<j且'p_1...p_{k-1}'='p_{j-k+1}...p_{j-1}'}& \text{集合不为空时}\\ 1& \text{其他情况} \end{cases} next[j]=⎩ ⎨ ⎧0Maxk∣1<k<j且′p1...pk−1′=′pj−k+1...pj−1′1j=1集合不为空时其他情况
例:
模式串:‘ababaa’
编号 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
S | a | b | a | b | a | a |
next | 0 | 1 | 1 | 2 | 3 | 4 |
当j=1,next[1]=0
当j=2,‘a’->next[2]=0+1=1
当j=3,‘ab’->next[3]=0+1=1
当j=4,‘aba’->next[4]=1+1=2
当j=5,‘abab’->next[5]=2+1=3
当j=6,‘ababa’->next[6]=3+1=4
void get_next(SString 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算法的平均时间复杂度:O(n+m)
int Index_KMP(SString S,SString T){
int i=1,j=1;
int next[MAXLEN];
get_next(T,next);
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;
}
}
3. KMP算法优化—nextval数组
KMP缺陷:当不匹配字符前方有大量的相同字符时,会增加无意义匹配。
解决方法:优化next数组为nextval数组。
nextval数组求解方法:
- 得到模式串的next数组
- 从编号为2的字符(假设为q)开始,如果对应的next的编号的字符(假设为p)与该字符(q)相同,将该字符(q)的nextval置为p的next值,如果不同则nextval[j]=next[j]。特别地,nextval[1]=0。
- 依次比较,直到模式串结束。
例:
模式串:‘ababaa’
编号 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
S | a | b | a | b | a | a |
next | 0 | 1 | 1 | 2 | 3 | 4 |
编号 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
S | a | b | a | b | a | a |
nextval | 0 | 1 | 0 | 1 | 0 | 4 |
void get_nextval(SString T,int next[],int nextval[]){
nextval[1]=0;
for (int j=2;j<=T.length;j++){
if (T.ch[next[j]]==T.ch[j])
nextval[j]=nextval[next[j]];
else
nextval[j]=next[j];
}
}