4.1串及其基本运算
4.1.1串的基本概念
串是由零个或多个任意字符组成的字符序列。一般记作:s=“s1 s2 … sn”
其中s是串名;在本书中,用双引号作为串的定界符,引号引起来的字符串列为串值,引号本身不属于串的内容;
ai(1<=i<=n)是一个任意字符,它称为串的元素,是构成串的基本单位,i是它在整个串中的序号。
n为串的长度,表示串中所包含的字符个数,当n=0时,称为空串,通常记作Ф。
·子串与主串:
串中任意连续的字符组成的子序列称为该串的子串。
包含子串的串相应地称为主串。
·子串的位置:
子串的第一个字符在主串中的序号称为子串的位置。
·串相等:
称两个串是相等的,是指两个串的长度相等且对应字符都相等。
4.1.2 串的基本运算
⒈求串长 StrLength(s):操作结果是求出串s的长度。
⒉串赋值 StrAssign(s1,s2):s1是一个串变量,s2或者是一个串常量,或者是一个串变量(通常s2是一个串常量时称为串赋值,是一个串变量称为串拷贝),操作结果是将s2的串值赋值给s1, s1原来的值被覆盖掉。
⒊连接操作 StrConcat (s1,s2,s) 或 StrConcat (s1,s2):两个串的连接就是将一个串的串值紧接着放在另一个串的后面,连接成一个串。前者是产生新串s,s1和s2不改变; 后者是在s1的后面联接s2的串值,s1改变, s2不改变。
⒋求子串SubStr(s,i,len):串s存在并且1≤i≤StrLength(s),0≤len≤StrLength(s)-i+1。操作结果是求得从串s的第i个字符开始的长度为len的子串。len=0得到的是空串。
⒌串比较 StrCmp(s1,s2):操作结果是若s1==s2,操作返回值为0;若s1<s2,返回值<0;若s1>s2,返回值>0。
⒍子串定位 StrIndex(s,t) :s为主串,t为子串,操作结果是若t∈s,则操作返回t在s中首次出现的位置,否则返回值为0。
⒎串插入 StrInsert(s,i,t):串s,t存在,且1≤i≤StrLength(s)+1。操作结果是将串t插入到串s 的第i个字符位置上,s的串值发生改变。
⒏串删除StrDelete(s,i,len):串s存在,并且1≤i≤StrLength(s),0≤len≤StrLength(s)-i+1。操作结果是删除串s中从第i个字符开始的长度为len的子串,s的串值改变。
⒐串替换 StrRep(s,t,r):串s,t,r存在且t不为空,操作结果是用串r 替换串s中出现的所有与串t相等的不重叠的子串,s的串值改变。
串的基本操作中前5个操作是最为基本的,它们不能用其他的操作来合成,因此通常将这5个基本操作称为最小操作集。
4.2串的定长顺序存储及基本运算
4.2.1串的定长存储
串的定长顺序存储:用一组地址连续的存储单元存储串值中的字符序列,所谓定长是指按预定义的大小,为每一个串变量分配一个固定长度的存储区。
例如:
#define MAXSIZE 256
char s[MAXSIZE];
则串的最大长度不能超过256。
4.2.2定长顺序串的基本运算
如何标识串的实际长度?
1.类似顺序表,用一个指针来指向最后一个字符,这样表示的串描述如下:
typedef struct
{
char data[MAXSIZE]
int curlen;
}SeqString;
定义一个串变量:SeqString s;
这样可以直接得到串的长度:s.curlen+1
2.在串尾存储一个不会在串中出现的特殊字符作为串的终结符,以此表示串的结尾。例如C语言中就是用’\0’来表示串的结束。
这种存储方法不能直接得到串的长度,是用来判断字符是否是’\0’来确定串是否结束,从而求得串的长度。
3.设定长串存储空间:char s[MAXSIZE+1];
用s[0]存放串的实际长度,串值存放在s[1]~s[MAXSIZE],字符的序号和存储位置一致,应用更为方便。
下面的基本运算采用方法2,设串结束用'\0'来标识。
主要讨论定长串联接、求子串、串比较算法,顺序串的插入和删除等运算基本与顺序表相同,在此不在赘述。
1.串链接
int StrConcat1(char s1[],char s2[],char s[]){
int i,j,len1,len2;
i=0;
len1=StrLength(s1);
len2=StrLength(s2);
if(len1+len2>MAXSIZE-1)
return 0;//串存储空间不够,返回错误代码0
j=0;
while(s1[j]!='\0')
{
s[i]=s1[j];//将s1串值赋给s
i++;
j++;
}
j=0;
while(s2[j]!='\0')
{
s[i]=s2[j];//将s2串值赋给s
i++;
j++;
}
return 1;
}
2.求子串
int StrSub(char *t,char *s,int i,int len){
//用t返回串s中第i个字符开始的长度为len的子串1<=i<=串长
int j;
int slen;
slen=StrLength(s);
if(i<1||i<slen||len<0||len>slen-i+1){
cout<<"参数不对"<<endl;
return 0;//所给参数不符合要求,返回错误代码0
}
for(j=0;j<len;j++)
t[j]=s[i+j-1];//将对应子串值赋给t
t[j]='\0';//建立t串结束标记
return 1;
}
3.串比较
int StrComp(char *s1,char *s2){
int i;
i=0;
while(s1[i]==s2[i]&&s1[i]!='\0')//两串对应位置字符比较
i++;
return(s1[i]-s2[i]);//返回首个对应位置不同的字符的ASCII码差值
}
4.2.3模式匹配
串的模式匹配即子串定位是一种重要的船运算。设s和t是给定的两个串,在主串s中查找子串t的过程称为模式匹配。如果在s中找到等于t的子串,则匹配成功,函数返回t在s中的首次出现的存储位置(序号),否则匹配失败,返回0。t也成为模式。
为了运算方便,设字符串采用定长存储,且用第三种方式表示串长,即串的长度存放在0号单元,串值从1号单元存放,这样字符序号与存储位置一致。
-
朴素模式匹配算法(Brute-Force算法)
求子串位置的定位函数Index( S, T, pos)
·模式匹配:子串的定位操作通常称作串的模式匹配。
·目标串:主串S。
·模式串:子串T。
·匹配成功:若存在T的每个字符依次和S中的一个连续字符序列相等,则称匹配成功。返回T中第一个字符在S中的位置。
·匹配不成功:返回0。
Brute-Force简称为BF算法,亦称简单匹配算法,其基本思路是:
从目标串s=“s1s2…sn"的第一个字符开始和模式串t=“t1t2…tm"中的第一个字符比较,若相等,则继续逐个比较后续字符;否则从目标串s的第二个字符开始重新与模式串t的第一个字符进行比较。依次类推,若从模式串s的第i个字符开始,每个字符依次和目标串t中的对应字符相等,则匹配成功,该算法返回i;否则,匹配失败,函数返回0。
算法思想如下:
①首先将s1与t1进行比较,若不同,就将s2与t1进行比较,…,直到s的某一个字符si和t1相同,再将它们之后的字符进行比较,若也相同,则如此继续往下比较,当s的某一个字符si与t的字符tj不同时,则s返回到本趟开始字符的下一个字符,即i-j+2,t返回到1,继续开始下一趟的比较;
②重复上述过程。若t中的字符全部比完,则说明本趟匹配成功,本趟的起始位置是i-j+1或i-t[0],否则,匹配失败。
int Stringdex_BF(char *s,char *t){//从串s的第一个字符开始找首次与串t相等的子串
int i,j;
i=1;
j=1;
while(i<=s[0]&&j<=t[0])//都没有遇到结束符
if(s[i]==t[j])//对应位置字符比较
{
i++;
j++;
}//相等,继续
else{
i=i-j+2;
j=1;
}//不等,回溯
if(j>t[0])
return(i-t[0]);//匹配成功,返回子串首字符存储位置
else
return 0;//匹配失败
}
上述算法中匹配是从s串的第一个字符开始的,有时算法要求从指定位置开始,这时算法的参数表中要加一个位置参数pos:StrIndex(char *s,int pos,char *t),比较的初始位置定位在pos处。上述算法可以看作是pos=1时的特殊情况。
字串定位算法
int Index(char *s,int pos,char *t)
{
i=pos;j=1;
while(i<=s[0]&&j<=t[0])
{
if(s[i]==t[i])
{
++i;
++j;
}
else
{
i=i-j+2;
j=1;
}
if(j>t[0]) return i=t[0];
else return 0;
}
}
时间复杂度分析:设串长度为n,串t长度为m。匹配成功的情况下,参考两种极端情况:
①在最好情况下,每趟不成功的匹配都发生在第一对字符比较时
设匹配发生在si处,则字符比较次数在前面i-1趟匹配中共比较了i-1次,第i趟成功的匹配共比较了m次,所以共比较了i-1+m次,所有匹配成功的可能共有n-m+1种,设从si开始与t串匹配成功的概率为pi,等概率情况下pi=1/(n-m+1),因此最好情况下平均比较的次数是:
时间复杂度是O(n+m)
②在最坏情况下,每趟不成功的匹配都发生在t的最后一对字符。
设匹配成功发生在si处,则在前面i-1趟匹配中共比较了(i-1)*m次,第i趟成功的匹配共比较了m次,所以共比较了i *m次,因此最坏情况下平均比较的次数是:
时间复杂度是O(n*m)
KMP算法
KMP算法是D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的,简称KMP算法。该算法较BF算法有较大改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
改进思想:
每趟匹配过程中出现字符比较不等时,不回溯主指针i,利用已得到的“部分匹配”结果将模式(即子串)向右滑动尽可能远的一段距离,继续进行比较。
定义next[j]函数,表明当模式中第j个字符与主串中相应字符“失配”时,在模式串中需要重新和主串中该字符进行比较的字符的位置,即指针j的回溯位置。
Next[j]即回溯位置k仅仅与模式串有关!
改进的模式匹配算法
int Index_KMP(char *s,int pos,char *t)
{
i=pos;
j=1;
while(i<s[0]&&j<t[0])
{
if(j==0||s[i]==t[j])//是第一个位置的模式串及目标串字符不匹配时,j=next[1]=0后的处理,i=2,j=1,即下一次从模目标串的第2个字符开始匹配
{
i++;
j++;
}
else j=next[j];//i不变,j回溯
}
if(j>t[0]) return i-t[0];
else return 0;
}
KMP算法中的next函数值求解算法
void get_next(char *t,int next[])
{
j=1;next[1]=0;k=0;
while(i<t[0])
{
if(j==0||t[j]==t[k])
{
j++;k++;
next[j]=k;
}
else
k=next[k];//t[j]不等于t[K]
}
}
KMP算法的时间复杂度
设主串s的长度为n,模式串t长度为m,在KMP算法中求next数组的时间复杂度为O(m),在后面的匹配中因主串s的下标不减即不回溯,比较次数可记为n,所以KMP算法总的时间复杂度为O(n+m)。
4.3串的堆存储结构
串名的存储映像
串名的存储映象是串名-串值内存分配对照表,也称为索引表。表的形式有多种表示,常见的串名-串值存储映象索引表有如下几种: 带串长度的索引表 末尾指针的索引表 带特征位的索引表
1.带串长度的索引表
索引项的结点类型为:
typedef struct
{ char name[MAXNAME]; /串名/
int length; /串长/
char *stradr; /起始地址/
} LNode;
2.末尾指针的索引表
索引项的结点类型为:
typedef struct
{ char name[MAXNAME]; /串名/
char *stradr,*enadr; /起始地址,末尾地址/ } ENode;
3.带特征的索引表
当一个串的存储空间不超过一个指针的存储空间时,可以直接将该串存在索引项的指针域,这样既节约了存储空间,又提高查找速度,但这时要加一个特征位tag以指出指针域存放的是指针还是串。
索引项的结点类型为:
typedef struct
{ char name[MAXNAME];
int tag; /特征位/
union /起始地址或串值/
{ char *stradr;
char value[4];
}uval;
} TNode;
堆存储结构
在应用程序中,参与运算的串变量之间的长度相差较大,并且操作中串值的长度变化也较大,因此为串变量预分配固定大小的空间不尽合理。 堆存储结构的基本思想:在内存中开辟能存储足够多的串、地址连续的存储空间作为应用程序中所有串的可利用存储空间,称为堆空间,如设store[SMAX+1]; 根据每个串的长度,动态的为每个串在堆空间里申请相应大小的存储区域,这个串顺序存储在所申请的存储区域中,当操作过程中若原空间不够了,可以根据串的实际长度重新申请,拷贝原串值后再释放原空间。
堆结构上的串运算仍然基于字符序列的复制进行,基本思想是:当需要产生一个新串时,要判断堆空间中是否还有存储空间,若有,则从free指针开始划出相应大小的区域为该串的存储区,然后根据运算求出串值,最后建立该串存储映象索引信息,并修改free指针。 设堆空间为: char store[SMAX+1]; 自由区指针:int free;
串的存储映象类型如下:
typedef struct
{ int length; /串长/
int stradr; /起始地址/
} HString;
基于堆结构的基本运算
1.串常量赋值
int StrAssign(HString *s1,char *s2){
//将一个字符型数组s2中的字符串送入堆store中,free是自由区的指针
int i,len;
i=0;
len=StrLength(s2);
if(len<0||free+len-1>SMAX)
return 0;//堆中自由区空间不够,返回错误代码0
else
{
for(i=0;i<len;i++)
store[free+i]=s2[i];//将s2串值赋值给堆
s1->stradr=free;//建立s1串存储映像索引信息
s1->length=len;
free=free+len;
}
}
2.赋值一个串
void StrCopy(Hstring *s1,Hstring s2)
/*该运算将堆store 中的一个串s2 复制到一个新串s1 中*/
{
int i;
if (free+s2.lengt-1>SMAX)
return error ;
else
{ for(i=0; i<s2.length;i++)
store[free+i]=store[s2.atradr+i];
s1->length=s2.length;
s1->stradr=free;
free=free+s2.length;
}
}
3.求子串
void StrSub(Hstring *t, Hstring s,int i,int len)
/*该运算将串s 中第i 个字符开始的长度为len 的子串送到一个新串t 中*/
{
int i;
if (i<0 || len<0 || len>s.len-i+1) return error ;
else
{
t->length=len;
t->stradr=s.stradr+i-1;
}
}
4.串连接
void Concat(s1,s2,s)
HString s1,s2;
HString *s;
{ HString t;
StrCopy (s,s1);
StrCopy (&t,s2);
s->length=s1.length+s2.length;
}
以上堆空间和算法是由算法编写者自己设计和编写来实现的,在这里,重点介这种存储的处理思想,很多问题及细节尚未涉及,比如,废弃串的回归、自由区的管理问题等等。在常用的高级语言及开发环境中,大多系统本身都提供了串的类型及大量的库函数,用户可直接使用,这样会使算法的设计和调试更方便容易,可靠性更高。