《数据结构与算法》第四章串
知识点及例题精选
串(即字符串)是一种特殊的线性表,它的数据元素仅由一个字符组成。
4.1 串及其基本运算
4.1.1 串的基本概念
1.串的定义
字符串长度17模式串长度8串是由零个或多个任意字符组成的字符序列。一般记作:
s="s1 s2 … s n""
其中s 是串名;在本书中,用双引号作为串的定界符,引号引起来的字符序列为串值,引号本身不属于串的内容;a i(1<=i<=n)是一个任意字符,它称为串的元素,是构成串的基本单位,i是它在整个串中的序号; n为串的长度,表示串中所包含的字符个数,当n=0时,称为空串,通常记为Ф。
2.几个术语
子串与主串:串中任意连续的字符组成的子序列称为该串的子串。包含子串的串相应地称为主串。
子串的位置:子串的第一个字符在主串中的序号称为子串的位置。
串相等:称两个串是相等的,是指两个串的长度相等且对应字符都相等。
4.2 串的定长顺序存储及基本运算
因为串是数据元素类型为字符型的线性表,所以线性表的存储方式仍适用于串,也因为字符的特殊性和字符串经常作为一个整体来处理的特点,串在存储时还有一些与一般线性表不同之处。
4.2.1 串的定长顺序存储
类似于顺序表,用一组地址连续的存储单元存储串值中的字符序列,所谓定长是指按预定义的大小,为每一个串变量分配一个固定长度的存储区,如:
#define MAXSIZE 256
char s[MAXSIZE];
则串的最大长度不能超过256。
如何标识实际长度?
1. 类似顺序表,用一个指针来指向最后一个字符,这样表示的串描述如下:
typedef struct
{ char data[MAXSIZE];
int curlen;
} SeqString;
定义一个串变量:SeqString s;
这种存储方式可以直接得到串的长度:s.curlen+1。如图4.1所示。
s.data
MAXSIZE-1
图4.1 串的顺序存储方式1
2. 在串尾存储一个不会在串中出现的特殊字符作为串的终结符,以此表示串的结尾。比如C 语言中处理定长串的方法就是这样的,它是用’\0’来表示串的结束。这种存储方法不能直接得到串的长度,是用判断当前字符是否是’\0’来确定串是否结束,从而求得串的长度。
char s[MAXSIZE];
图4.2 串的顺序存储方式2
3. 设定长串存储空间:char s[MAXSIZE+1]; 用s[0]存放串的实际长度,串值存放在s[1]~s[MAXSIZE],字符的序号和存储位置一致,应用更为方便。
4.2.2 定长顺序串的基本运算
本小节主要讨论定长串联接、求子串、串比较算法,顺序串的插入和删除等运算基本与顺序表相同,在此不在赘述。串定位在下一小节讨论,设串结束用'\0'来标识。
1.串联接:把两个串s1和s2首尾连接成一个新串s ,即:s<=s1+s2。
int StrConcat1(s1,s2,s)
char s1[],s2[],s[];
{ int i=0 , j, len1, len2;
len1= StrLength(s1); len2= StrLength(s2)
if (len1+ len2>MAXSIZE-1) return 0 ; /* s 长度不够*/
j=0;
while(s1[j]!=’\0’) { s[i]=s1[j];i++; j++; }
j=0;
while(s2[j]!=’\0’) { s[i]=s2[j];i++; j++; }
s[i]=’\0’; return 1;
}
算法 4.1
2.求子串
int StrSub (char *t, char *s, int i, int len)
/* 用t 返回串s 中第个i 字符开始的长度为len 的子串1≤i ≤串长*/
{ int slen;
slen=StrLength(s);
if ( i<1 || i>slen || len<0 || len>slen-i+1)
{ printf("参数不对"); return 0; }
for (j=0; j<len; j++)
t[j]=s[i+j-1];
MAXSIZE-1 s.curlen
t[j]=’\0’;
return 1;
}
算法 4.2
3.串比较
int StrComp(char *s1, char *s2)
{ int i=0;
while (s1[i]==s2[i] && s1[i]!=’\0’) i++;
return (s1[i]-s2[i]);
}
算法 4.3
4.2.3 模式匹配
串的模式匹配即子串定位是一种重要的串运算。设s和t是给定的两个串,在主串s中到等于子串t的过程称为模式匹配,如果在s中到等于t的子串,则称匹配成功,函数返回t在s中的首次出现的存储位置(或序号),否则匹配失败,返回-1。t也称为模式。为了运算方便,设字符串的长度存放在0号单元,串值从1号单元存放,这样字符序号与存储位置一致。
1.简单的模式匹配算法
算法思想如下:首先将s1与t1进行比较,若不同,就将s2与t1进行比较,...,直到s 的某一个字符s i和t1相同,再将它们之后的字符进行比较,若也相同,则如此继续往下比较,当s的某一个字符s i与t的字符t j不同时,则s返回到本趟开始字符的下一个字符,即s i-j+2,t返回到t1,继续开始下一趟的比较,重复上述过程。若t中的字符全部比完,则说明本趟匹配成功,本趟的起始位置是i-j+1或i-t[0],否则,匹配失败。
设主串s="ababcabcacbab",模式t="abcac",匹配过程如图4.3所示。
第一趟
第二趟
第三趟
第四趟
第五趟
第六趟
图4.3 简单模式匹配的匹配过程
依据这个思想,算法描述如下:
int StrIndex_BF (char *s,char *t)
/*从串s 的第一个字符开始首次与串t 相等的子串*/
{ int 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 –1;
}
算法 4.4
该算法简称为BF 算法。下面分析它的时间复杂度,设串s 长度为n ,串t 长度为m 。 匹配成功的情况下,考虑两种极端情况:
在最好情况下,每趟不成功的匹配都发生在第一对字符比较时:
例如:s="aaaaaaaaaabc "
t="bc "
设匹配成功发生在s i 处,则字符比较次数在前面i-1趟匹配中共比较了i-1次,第i 趟成功的匹配共比较了m 次,所以总共比较了i-1+m 次,所有匹配成功的可能共有n-m+1种,设从s i 开始与t 串匹配成功的概率为p i ,在等概率情况下p i =1/(n-m+1),因此最好情况下平均比较的次数是:
即最好情况下的时间复杂度是O(n+m)。
在最坏情况下,每趟不成功的匹配都发生在t 的最后一个字符:
例如:s="aaaaaaaaaaab "
t="aaab "
设匹配成功发生在s i 处,则在前面i-1趟匹配中共比较了(i-1)*m 次,第i 趟成功的匹配共比较了m 次,所以总共比较了i*m 次,因此最坏好情况下平均比较的次数是:
即最坏情况下的时间复杂度是O(n*m)。
上述算法中匹配是从s 串的第一个字符开始的,有时算法要求从指定位置开始,这时算法的参数表中要加一个位置参数pos :StrIndex(shar *s,int pos,char *t),比较的初始位置定位2)()1()1(111111m n m i m i p m n i m n m n i i +=+-⨯=+-⨯∑∑+-=+-+-= 2)2()()(111111+-⨯=⨯⨯=⨯⨯∑
∑+-=+-+-=m n m m i m i p m n i m n m n i i
在pos处。算法4.4是pos=1的情况。
*2.改进后的模式匹配算法
BF算法简单但效率较低,一种对BF算法做了很大改进的模式匹配算法是克努特(Knuth),莫里斯(Morris)和普拉特(Pratt)同时设计的,简称KMP算法。
(1)KMP算法的思想
分析算法4.4的执行过程, 造成BF算法速度慢的原因是回溯,即在某趟的匹配过程失败后,对于s串要回到本趟开始字符的下一个字符,t串要回到第一个字符。而这些回溯并不是必要的。如图4.3所示的匹配过程,在第三趟匹配过程中,s3 ~ s6和t1~ t4是匹配成功的,s7≠t5匹配失败,因此有了第四趟,其实这一趟是不必要的:由图可看出,因为在第三趟中有s4=t2,而t 1≠t2,肯定有t1≠s4。同理第五趟也是没有必要的,所以从第三趟之后可以直接到第六趟,进一步分析第六趟中的第一对字符s6和t1的比较也是多余的,因为第三趟中已经比过了s6和t4,并且s6=t4,而t 1=t4,必有s 6=t1,因此第六趟的比较可以从第二对字符s7和t2开始进行,这就是说,第三趟匹配失败后,指针i不动,而是将模式串t向右“滑动”,用t2“对准”s 7继续进行,依此类推。这样的处理方法指针i 是无回溯的。
综上所述,希望某趟在s i和t j匹配失败后,指针i不回溯,模式t向右“滑动”至某个位置上,使得t k对准s i 继续向右进行。显然,现在问题的关键是串t“滑动”到哪个位置上?不妨设位置为k,即s i和t j匹配失败后,指针i不动,模式t向右“滑动”,使t k和s i对准继续向右进行比较,要满足这一假设,就要有如下关系成立:
"t1 t2 … t k-1 "="s i-k+1 s i-k+2 … s i-1 "(4.1)
(4.1)式左边是t k前面的k-1个字符,右边是s i前面的k-1个字符。
而本趟匹配失败是在s i和t j之处,已经得到的部分匹配结果是:
"t1 t2 … t j-1 "="s i-j+1 s i-j+2 … s i-1 "(4.2)
因为k<j,所以有:
"t j-k+1 t j-k+2 … t j-1 "="s i-k+1 s i-k+2 … s i-1 "(4.3)
(4.3)式左边是t j前面的k-1个字符,右边是s i前面的k-1个字符,
通过(4.1)和(4.3)得到关系:
"t1 t2 … t k-1 "="t j-k+1 t j-k+2 … t j-1 "(4.4)
结论:某趟在s i和t j匹配失败后,如果模式串中有满足关系(4)的子串存在,即:模式中的前k-1个字符与模式中t j字符前面的k-1个字符相等时,模式t就可以向右“滑动”至使t k和s i对准,继续向右进行比较即可。
(2)next函数
模式中的每一个t j都对应一个k值,由(4.4)式可知,这个k值仅依赖与模式t本身字符序列的构成,而与主串s无关。我们用next[j]表示t j对应的k值,根据以上分析,next函数有如下性质:
①next[j]是一个整数,且0≤next[j]<j
②为了使t的右移不丢失任何匹配成功的可能,当存在多个满足(4.4)式的k 值时,应
取最大的,这样向右“滑动”的距离最短,“滑动”的字符为j-next[j]个。
③如果在t j前不存在满足(4.4)式的子串,此时若t1≠t j,则k=1; 若t1=t j,则k=0; 这
时“滑动”的最远,为j-1个字符即用t1 和s j+1继续比较。
因此,next函数定义如下:
0 当j=1
max {k | 1<=k<j 且"t1 t2 … t k-1 "="t j-k+1 t j-k+2 … t j-1 "next[j]=
1 当不存在上面的k且t1≠t j
0 当不存在上面的k且t1=t j
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论