第四章 串
串的定义及基本操作
串的定义
串,即字符串(String)是由零个或多个字符组成的有限序列。
串vs线性表
串是一种特殊的线性表,数据元素之间呈线性关系;
串的数据对象限定为字符集(如中文字符、英文字符、数字字符、标点字符等);
串的基本操作,如增删改查等通常以子串为操作对象。
串的基本操作
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返回由S1和S2联接而成的新串
SubString(&Sub,S,pos,len):求子串。
用Sub返回串S的第pos个字符起长度为len的子串。
Index(S,T):定位操作。
若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置;否则函数值为0。
StrCompare(S,T):比较操作。
若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
串的比较操作
比较函数:
StrCompare(S,T):比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
比较方式:
从第一个字符开始往后依次对比,先出现更大字符的串就更大:“abandon” < “aboard”;
长串的前缀与短串相同时,长串更大:“abstract” < “abstraction”;
只有两个串完全相同时,才相等:“academic”=“academic”。
串的存储结构
顺序存储
#define MAXLEN 255 //预定义最大串长为255
//静态数组实现(定长顺序存储):
typedef struct{
char ch[MAXLEN];//每个分量存储一个字符
int length;//串的实际长度
}SString;
//动态数组实现(堆分配存储):
typedef struct{
char *ch;//按串长分配存储区,ch指向串的基地址
int length;//串的长度
}HString;
HString S;
S.ch = (char *) malloc(MAXLEN * sizeof(char));
S.length = 0;
//用完需要手动free
顺序存储的几种存储方案
通常采用方案四。
链式存储
每个结点存1个字符:
typedef struct StringNode{
char ch;
struct StringNode * next;
}StringNode, * String;
每个结点存多个字符
typedef struct StringNode{
char ch[4];
struct StringNode * next;
}StringNode, * String;
基本操作的实现
采用静态数组,方案四:
#define MAXLEN 255 //预定义最大串长为255
//静态数组实现(定长顺序存储):
typedef struct{
char ch[MAXLEN];//每个分量存储一个字符
int length;//串的实际长度
}SString;
SubString(&Sub,S,pos,len):求子串
//求子串
bool SubString(SString &Sub, SString S, int pos,int len){
//子串范围越界
if (pos+1en-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;
}
StrCompare(S,T):比较操作
//比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0
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;
}
Index(S,T):定位操作
//定位操作
int Index(SString S, SString T){
int i=1, n=StrLength(S), m=StrLength(T);
SString sub; //用于暂存子串
while(i<=n-m+1){
SubString(sub,S,i,m);
if(StrCompare(sub, T)!=0) ++i;
else return i; //返回子串在主串中的位置
}
return 0; //S中不存在与T相等的子串
}
朴素(简单)模式匹配算法
其实就是定位操作:
int Index(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);(通常情况下,n远大于m)
匹配成功/失败最多需要(n-m+1)*m次比较,所以最坏时间复杂度为O(nm)
以下为王道课本上的代码:
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;//注意此处直接用i和j来表示
j=1;//指针后退重新开始匹配
}
}
if(j>T.length)
return i-T.length;
else
return 0;
}
KMP算法-朴素模式匹配算法的优化
朴素模式匹配算法的缺点:
当某些子串与模式串能部分匹配时,主串的扫描指针 i 经常回溯,导致时间开销增加
**改进思路:**主串指针不回溯,只有模式串指针回溯
优化情况分析:
…
若当前两个字符匹配,则 i++,j++;
若 j=1 时发生不匹配,则应让 i++,而 j 依然是1(先令j=0,再进行 j++);
若 j=2 时发生不匹配,则应让 j 回到 1;
若 j=3 时发生不匹配,则应让 j 回到 1;
若 j=4 时发生不匹配,则应让 j 回到 1 ;
若 j=5 时发生不匹配,则应让 j 回到 2 ;
若 j=6 时发生不匹配,则应让 j 回到 1。
KMP算法代码
int Index_KMP(SString S,SString 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;
}
KMP算法-求next数组
概念:
next 数组:当模式串的第 j 个字符匹配失败时,令模式串跳到第next[j]个字符的再继续匹配;
串的前缀:包含第一个字符,且不包含最后一个字符的子串
串的后缀:包含最后一个字符,且不包含第一个字符的子串
当第j个字符匹配失败,由前 1~j-1个字符组成的串记为S。
S的最长相等前后缀长度:
例:‘aba’的前缀有:‘a’、‘ab’,后缀有:‘a’、‘ba’,所以‘aba’的最长相等前后缀长度为1;同理‘abab’的最长相等前后缀长度为2;‘ababa’的最长相等前后缀长度为3…
next[j]= S的最长相等前后缀长度 +1
特别地,next[1]=0
例如:当S=‘ababaa’时,
序号j | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
模式串 | a | b | a | b | a | a |
next[ j ] | 0 | 1 | 1 | 2 | 3 | 4 |
当S=‘aaaab’时,
序号j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
模式串 | a | a | a | a | b |
next[ j ] | 0 | 1 | 2 | 3 | 4 |
算法代码
KMP算法:当子串和模式串不匹配时,主串指针 i
不回溯,模式串指针 j=next[j]
算法平均时间复杂度:O(n+m)
//求模式串T的next数组
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;
//若pi=pj,则next[j+1]=next[j]+1
next[i]=j;
}
else
//否则令j=next[j],循环继续
j=next[j];
}
}
//KMP算法
int Index_ _KMP(SString S,SString T){
int i=1, j=1;
int next [T.length+1];
get_next(T,next); //求模式串的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;
}
KMP算法优化—nextval数组
KMP算法存在的问题:
当j=4
时,g
与l
比较,不等,下一步是j=1
,但j=1
时依然是g
与l
比较,就进行了一次无意义的对比。
就此引入nextval[j]
数组:
nextval数组的求法:
- 先算出next数组
- 先令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];
}
KMP算法优化:当子串和模式串不匹配时j=nextval[j];