chunlanse2014

导航

4.2 串的定长顺序存储及基本运算

因为串是数据元素类型为字符型的线性表,所以线性表的存储方式仍适用于串,也因为字符的特殊性和字符串经常作为一个整体来处理的特点,串在存储时还有一些与一般线性表不同之处。

 

4.2.1 串的定长顺序存储

类似于顺序表,用一组地址连续的存储单元存储串值中的字符序列,所谓定长是指按预定义的大小,为每一个串变量分配一个固定长度的存储区,如:

1 #define MAXSIZE 256
2 char s[MAXSIZE];

则串的最大长度不能超过256。

如何标识实际长度?

1. 类似顺序表,用一个指针来指向最后一个字符。这样表示的串描述如下:

1 typedef struct
2 { 
3     char data[MAXSIZE];
4     int curlen;
5 } SeqString;

定义一个串变量:SeqString s。这种存储方式可以直接得到串的长度:s.curlen+1。如图4.1 所示。

2. 在串尾存储一个不会在串中出现的特殊字符作为串的终结符,以此表示串的结尾。比如C 语言中处理定长串的方法就是这样的,它是用’\0’来表示串的结束。这种存储方法不能直接得到串的长度,是用判断当前字符是否是’\0’来确定串是否结束,从而求得串的长度。

3. 设定长串存储空间:char s[MAXSIZE+1]; 用s[0]存放串的实际长度,串值存放在s[1]~s[MAXSIZE],字符的序号和存储位置一致,应用更为方便。

 

4.2.2 定长顺序串的基本运算

本小节主要讨论定长串连接、求子串、串比较算法,顺序串的插入和删除等运算基本与顺序表相同,在此不在赘述。串定位在下一小节讨论,设串结束用'\0'来标识。

1.串连接:把两个串s1 和s2 首尾连接成一个新串s ,即:s<=s1+s2。

 1 int StrConcat1(s1,s2,s)
 2 {
 3     char s1[],s2[],s[];
 4     int i=0 , j, len1, len2;
 5     len1= StrLength(s1); len2= StrLength(s2)
 6     if (len1+ len2>MAXSIZE-1) 
 7         return -1 ; /* s 长度不够*/
 8     j=0;
 9     while(s1[j]!=’\0’) 
10     { 
11         s[i]=s1[j];
12         i++; 
13         j++; 
14     }
15     j=0;
16     while(s2[j]!=’\0’) 
17     { 
18         s[i]=s2[j];
19         i++; 
20         j++; 
21     }
22     s[i]=’\0’; 
23     return 0;
24 }

算法4.1

2.求子串

 1 int StrSub (char *t, char *s, int i, int len)
 2 /* 用t 返回串s 中第个i 字符开始的长度为len 的子串1≤i≤串长*/
 3 { 
 4     int slen;
 5     slen=StrLength(s);
 6     if ( i<1 || i>slen || len<0 || len>slen-i+1)
 7     { 
 8         printf("参数不对"); 
 9         return -1; 
10     }
11     for (j=0; j<len; j++)
12         t[j]=s[i+j-1];
13     t[j]=’\0’;
14     return 0;
15 }

算法4.2

3.串比较

1 int StrComp(char *s1, char *s2)
2 { 
3     int i=0;
4     while (s1[i]==s2[i] && s1[i]!=’\0’) 
5         i++;
6     return (s1[i]-s2[i]);
7 }

算法4.3

 

4.2.3 模式匹配

串的模式匹配即子串定位是一种重要的串运算。设s 和t 是给定的两个串,在主串s中找到等于子串t 的过程称为模式匹配,如果在s 中找到等于t 的子串,则称匹配成功,函数返回t 在s 中的首次出现的存储位置(或序号),否则匹配失败,返回-1。t 也称为模式。为了运算方便,设字符串的长度存放在0 号单元,串值从1 号单元存放,这样字符序号与存储位置一致。

1.简单的模式匹配算法
算法思想如下:首先将s1 与t1 进行比较,若不同,就将s2 与t1 进行比较,...,直到s的某一个字符si 和t1 相同,再将它们之后的字符进行比较,若也相同,则如此继续往下比较,当s 的某一个字符si 与t 的字符tj 不同时,则s 返回到本趟开始字符的下一个字符,即si-j+2,t 返回到t1,继续开始下一趟的比较,重复上述过程。若t 中的字符全部比完,则说明本趟匹配成功,本趟的起始位置是i-j+1 或i-t[0],否则,匹配失败。设主串s="ababcabcacbab",模式t="abcac",匹配过程如图4.3 所示。

依据这个思想,算法描述如下:

 1 int StrIndex_BF (char *s,char *t)
 2  /*从串s 的第一个字符开始找首次与串t 相等的子串。若不存在,则函数返回值为0。*/
 3 { 
 4     int i=1,j=1;              /*i用于主串s中当前位置下标值,j用于子串t中当前位置下标值*/
 5     while (i<=s[0] && j<=t[0] ) /*若i小于s的长度且j小于t的长度时循环*/
 6     {
 7         if (s[i]==t[j])         /*两字母相等则继续*/
 8         { 
 9             i++;         
10             j++; 
11         }
12         else                /*指针后退重新开始匹配*/
13         {
14             i=i-j+2;      /*i退回到上次匹配首位的下一位*/
15             j=1;          /*j退回到子串t的首位*/
16         }
17     }
18     if (j>t[0]) 
19         return (i-t[0]); /*匹配成功,返回存储位置*/
20     else 
21         return 0; 
22 }

算法4.4

该算法简称为BF 算法。下面分析它的时间复杂度,设串s 长度为n,串t 长度为m。匹配成功的情况下,考虑两种极端情况:在最好情况下,每趟不成功的匹配都发生在第一对字符比较时:
例如:

       s="aaaaaaaaaabc"
       t="bc"
设匹配成功发生在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 的最后一个字符:
例如:

       s="aaaaaaaaaaab"
       t="aaab"
设匹配成功发生在si 处,则在前面i-1 趟匹配中共比较了(i-1)*m 次,第i 趟成功的匹配共比较了m 次,所以总共比较了i*m 次,因此最坏好情况下平均比较的次数是:

即最坏情况下的时间复杂度是O(n*m)。

上述算法中匹配是从s 串的第一个字符开始的,有时算法要求从指定位置开始,这时算法的参数表中要加一个位置参数pos:StrIndex(char *s,char *t,int pos),比较的初始位置定位在pos 处。算法4.4 是pos=1 的情况。

2.改进后的模式匹配算法
BF 算法简单但效率较低,一种对BF 算法做了很大改进的模式匹配算法是克努特(Knuth),莫里斯(Morris)和普拉特(Pratt)同时设计的,简称KMP 算法。

(1) KMP 算法的思想
分析算法4.4 的执行过程, 造成BF 算法速度慢的原因是回溯,即在某趟的匹配过程失败后,对于s 串要回到本趟开始字符的下一个字符,t 串要回到第一个字符。而这些回溯并不是必要的。

如图4.3 所示的匹配过程,在第三趟匹配过程中,s3 ~ s6t1~ t4 是匹配成功的,s7≠t5 匹配失败,因此有了第四趟,其实这一趟是不必要的:

由图可看出,因为在第三趟中有s4=t2,而t1≠t2,肯定有t1≠s4

同理第五趟(第三趟有s5=t3,而t1≠t3,则有t1≠s5)也是没有必要的。

所以从第三趟之后可以直接到第六趟,进一步分析第六趟中的第一对字符s6 和t1 的比较也是多余的。

因为第三趟中已经比过了s6 和t4,并且s6=t4,而t1=t4,必有s6=t1,因此第六趟的比较可以从第二对字符s7 和t2 开始进行。

这就是说,第三趟匹配失败后,指针i 不动,而是将模式串t向右“滑动”,用t2 “对准” s7 继续进行,依此类推。这样的处理方法指针i 是无回溯的。



综上所述,希望某趟在si 和tj 匹配失败后,指针i 不回溯,模式t 向右“滑动”至某个位置上,使得tk 对准s i 继续向右进行。显然,现在问题的关键是串t“滑动”到哪个位置上?不妨设位置为k,即si 和tj 匹配失败后,指针i 不动,模式t 向右“滑动”,使tk 和si对准继续向右进行比较,要满足这一假设,就要有如下关系成立:

"t1 t2 … tk-1 " ="si-k+1 si-k+2 … si-1 " (4.1)
假设以图4.3第三趟为例:i=7,j=5,即s7和t5匹配失败,由以上分析,设位置k=2,则应满足上式:t1=s6

(4.1)式左边是tk 前面的k-1 个字符,右边是si 前面的k-1 个字符。而本趟匹配失败是在si 和tj 之处,已经得到的部分匹配结果是:

"t1 t2 … tj-1 " ="si-j+1 si-j+2 … si-1 " (4.2)
对应:"t1 t2 t3 t4"="s3 s4 s5 s6"

因为k<j,所以有:

"tj-k+1 tj-k+2 … tj-1 " ="si-k+1 si-k+2 … si-1 " (4.3)
对应:t4=s6

(4.3)式左边是tj 前面的k-1 个字符,右边是si 前面的k-1 个字符,通过(4.1)和(4.3)得到关系:

"t1 t2 … tk-1 " ="tj-k+1 tj-k+2 … tj-1 " (4.4)

对应:t1=t4
结论:某趟在si 和tj 匹配失败后,如果模式串中有满足关系(4)的子串存在,即:模式中的前k-1 个字符与模式中tj 字符前面的k-1 个字符相等时,模式t 就可以向右“滑动”至使tk 和si 对准,继续向右进行比较即可。

(2)next 函数
模式中的每一个tj 都对应一个k 值,由(4.4)式可知,这个k 值仅依赖与模式t 本身字符序列的构成,而与主串s 无关。

我们用next[j]表示tj 对应的k 值,根据以上分析,next函数有如下性质:
① next[j]是一个整数,且0≤next[j]<j
② 为了使t 的右移不丢失任何匹配成功的可能,当存在多个满足(4.4)式的k 值时,应取最大的,这样向右“滑动”的距离最短,“滑动”的字符为j-next[j]个。
③ 如果在tj 前不存在满足(4.4)式的子串,此时若t1≠tj,则k=1; 若t1=tj,则k=0; 这时“滑动”的最远,为j-1 个字符即用t1 和sj+1 继续比较。

因此,next 函数定义如下:

1)当j=1时,next[1]=0;

2)当j=2时,j由1到j-1只有字符"a",属于其他情况next[2]=1;

3)当j=3时,j由1到j-1串是"ab",显然"a"与"b"不相等,属于其他情况,next[3]=1;

4)当j=4时,j由1到j-1串是"abc",显然"a"与"c"不相等,属于其他情况,next[4]=1;

5)当j=5时,j由1到j-1串是“abca”,前缀字符“a”与后缀字符“a”相等,因此可推算出k的值为2,因此next[5]=2;

6)当j=6时,j由1到j-1串是“abcab”,由于前缀字符“ab”和后缀“ab”相等,所以next[6]=3。

总结:如果前后缀一个字符相等,k值是2,两个字符k值是3,n个相等k值就是n+1。

(3) KMP 算法
在求得模式的next 函数之后,匹配可如下进行:

假设以指针i 和j 分别指示主串和模式中的比较字符,令i 的初值为pos,j 的初值为1。

若在匹配过程中si≠tj,则i 和j 分别增1,若si≠tj 匹配失败后,则i 不变,j 退到next[j]位置再比较,若相等,则指针各自增1,否则j 再退到下一个next 值的位置,依此类推。

直至下列两种情况:

一种是j 退到某个next值时字符比较相等,则i 和j 分别增1继续进行匹配;

另一种是j 退到值为零(即模式的第一个字符失配),则此时i 和j 也要分别增1,表明从主串的下一个字符起和模式重新开始匹配。

设主串s="acabaabaabcacaabc",子串t="abaabcac",图4.4 是一个利用next 函数进行匹配的过程示意图。在假设已有next 函数情况下,KMP 算法如下:

 1 int StrIndex_KMP(char *s,char *t,int pos)
 2 /*从串s 的第pos 个字符开始找首次与串t 相等的子串。返回子串t在主串s中第pos个字符之后的位置。若不存在,则函数返回值为0。*/
 3 { 
 4     int i=pos,j=1; /*i用于主串s当前位置下标值,j用于子串t中当前位置下标值*/
 5     int next[255];  /*定义一next数组*/
 6     GetNext(t,next);  /*对串t作分析,得到next数组*/
 7     while (i<=s[0] && j<=t[0] ) /*若i小于s的长度且j小于t的长度时,循环继续*/
 8     {    
 9         if (j==0||s[i]==t[j])  /*两字母相等则继续,与朴素算法增加了j=0判断*/
10         { 
11             i++; 
12             j++; 
13         }
14         else                      /*指针后退重新开始匹配*/
15             j=next[j];      /*j退回合适的位置,i值不变*/
16     }    
17     if (j>t[0]) 
18         return i-t[0]; /*匹配成功,返回存储位置*/
19     else 
20         return 0;
21 }

算法4.5

 


(4)如何求next 函数
由以上讨论知,next 函数值仅取决于模式本身而和主串无关。

我们可以从分析next 函数的定义出发用递推的方法求得next 函数值。由定义知:
next[1]=0 (4.5)
设next[j]=k,即有:

"t1 t2 … tk-1 " ="tj-k+1 tj-k+2 … tj-1" (4.6)

next[j+1]=? 可能有两种情况:

第一种情况:若tk =tj 则表明在模式串中

"t1 t2 … tk " ="tj-k+1 tj-k+2 … tj " (4.7)

这就是说next[j+1]=k+1,即

next[j+1]=next[j]+1 (4.8)

第二种情况:若tk ≠tj 则表明在模式串中

"t1 t2 … tk "≠"tj-k+1 tj-k+2 … tj " (4.9)

此时可把求next 函数值的问题看成是一个模式匹配问题,整个模式串既是主串又是模式,而当前在匹配的过程中,已有(4.6)式成立,则当tk ≠tj 时应将模式向右滑动,使得第next[k]个字符和“主串”中的第j 个字符相比较。

若next[k]=k′,且t k′=tj,则说明在主串中第j+1 个字符之前存在一个最大长度为k′的子串,使得

"t1 t2 … t k′ "="tj-k′+1 tj-k′+2 … tj " (4.10)


因此: next[j+1]=next[k]+1 (4.11)
同理若t k′ ≠tj,则将模式继续向右滑动至使第next[k′]个字符和tj 对齐。

依此类推,直至tj 和模式中的某个字符匹配成功或者不存在任何k′(1< k′<k <…<j)满足(4.10)。

此时若t1≠tj+1 , 则有:next[j+1]=1 (4.12)
否则若t1=tj+1 ,则有:next[j+1]=0 (4.13)

综上所述,求next 函数值过程的算法如下:

void GetNext(char *t,int next[])
/*求模式t 的next 值并存入next 数组中*/
{
    int i=1,j=0;
    next[1]=0;
    while (i<t[0])    /*此处t[0]表示串t的长度*/
    { 
        if (j==0||t[i]==t[j])     /*t[i]表示后缀的单个字符,t[j]表示前缀的单个字符*/
        {
            i++; 
            j++;
            next[i]=j;
        } 
        else 
            j=next[j];     /*若字符不相同,则j值回溯*/
    }
}

算法4.6

算法4.6 的时间复杂度是O(m);所以算法4.5 的时间复杂度是O(n*m),但在一般情况下,实际的执行时间是O(n+m)。

当然KMP 算法和简单的模式匹配算法相比,增加了很大难度,我们主要学习该算法的设计技巧。

posted on 2015-05-15 21:15  chunlanse2014  阅读(1414)  评论(0编辑  收藏  举报