以下是串的相关概念
注意位序从1开始而不是从0开始
串是一种特殊的线性表。串的数据对象限定为字符集(如中文字符,英文字符,数字字符,标点字符等)。但是相比线性表,串的基本操作一般以子串为操作对象。
操作 | 描述 |
---|---|
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 |
#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;
不同教材里顺序存储实现的细节会有些不同
方案一是我们刚才提到的方案,即申请一个数组并有专门的一个int类型变量记录字符串长度
方案二的优点是字符的位序和数组下标相同。缺点是由于数组是char型的,因此字符串的长度要被限制在255以内,否则char[0]表示不了那么多的数字。
方案三不记录串的长度,而是在串的末尾处插入特殊字符’\0’(对应ASCII码的0)。这种方案的缺点是想要知道串的长度需要从头到尾进行遍历
方案四是教材的方案。即舍弃char[0]不用,设置额外的int类型变量来记录字符串的长度
typedef struct StringNode{
char ch; //每个结点存1个字符
struct StringNOde *next;
}StringNode,*String;
这种存储方式每个字符占1B,每个指针4B,因此存储密度较低。因此可以适当改进,让存储密度提高
typedef struct StringNode{
char ch[4]; //每个结点存多个字符,没有字符的位置用'#'或'\0'补足
struct StringNode *next;
}StringNode,*String;
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.lengrh=len;
return true;
}
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;
}
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;
}
}
课本的代码实现
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{
return 0;
}
}
设模式串长度为m,主串长度为n,则
第一次匹配就成功
全部匹配失败且每个子串的第1个个字符就与模式串不匹配。要匹配n-m+1次,时间复杂度为O(n-m+1)=O(n-m),考虑到许多情况下主串长度远大于模式串长度,时间复杂度可进一步约等于O(n)
每个子串的前m-1个字符都和模式串匹配,只有第m个字符不匹配。匹配成功/匹配失败最多需要(n-m+1)*m次比较。
朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描制针i经常回溯,造成时间开销增大。KMP算法的改进思路是当子串和模式串不匹配时,主串指针i不回溯,只有模式串指针按照j=next[j]回溯
j=1时发现不匹配则令j=next[1]=0,但是右边却说要让j依然是1,这样设计是为了代码实现。我们可以先令j=0,再让i和j都同时++,这样就能让指针i往后移一位,同时j也保持为1
KMP算法的关键在于搞出一个和模式串相对应的数组next(数组怎么求等下再来探讨)
注意为什么要把next[1]设为0,我们可以利用这个信息做一个特殊的判断,当j=0时就说明主串的指针i应该往右移动了
代码实现如下
//求模式串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数组,时间复杂度O(m)
while(i<=S.length&&j<=T.length){ //时间复杂度O(n)
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算法平均时间复杂度:O(m+n)
利用nextval数组替换next数组进行优化,可以减少无意义的对比