字符串的朴素模式和KMP模式匹配

先复习一下字符串指针:

 1 #include <iostream>
 2 #include <string.h>
 3 using namespace std;
 4 
 5 int main()
 6 {
 7     char *p;
 8     char a[7]="google";
 9     p=a;
10 
11     cout<<p<<endl;//输出google,而不是输出地址
12     cout<<*p<<endl;//输出g
13     p++;
14     cout<<*p<<endl;//输出o
15 
16     cout<<a<<endl;//输出google
17     cout<<*a<<endl;//输出g
18     cout<<*(a+1)<<endl;//输出o,不能a++
19 
20     cout<<strlen(p)<<endl;//输出5
21     cout<<strlen(a)<<endl;//输出6
22 }

一、朴素模式匹配

对《大话数据结构》P131~P134—朴素模式匹配算法,进行了自己的理解并完善了代码。

代码和解释如下(VS2012测试通过):

 1 #include <iostream>
 2 #include <string.h>
 3 using namespace std;
 4 
 5 #define OK 1
 6 #define ERROR 0
 7 #define MAXSIZE 11
 8 
 9 typedef int Status;
10 
11 //T是字符数组,pchar是指向字符类型的指针,把pchar指向的字符串内容复制给T
12 Status StrAssign(char T[MAXSIZE+1],char *pchar)
13 { 
14     int i;
15     if(strlen(pchar)>MAXSIZE)//如果要拷贝的内容大于数组能存放的内容
16         return ERROR;
17     else
18     {
19         T[0]=strlen(pchar);//T[0]放串的长度
20         for(i=1;i<=T[0];i++)//循环T[0]次,即拷贝每个字符
21             T[i]=*(pchar+i-1);//-1是因为要从chars的第一个字符char[0]开始拷贝
22         return OK;
23     }
24 }
25 
26 //朴素的模式匹配法
27 int Index(char S[MAXSIZE+1],char T[MAXSIZE+1], int pos) 
28 {
29     int i = pos;//从主串S的pos位置开始匹配
30     int j = 1;//j用于子串T中当前位置下标值
31     while (i <= S[0] && j <= T[0])//若i小于S的长度并且j小于T的长度,循环继续
32     {
33         if (S[i] == T[j])//两字母相等则继续
34           {
35             ++i;
36              ++j; 
37           } 
38           else //指针后退重新开始匹配 
39           {  
40              i = i-j+2;//i退回到上次匹配首位的下一位
41              j = 1; //j退回到子串T的首位
42           }      
43     }
44     if (j > T[0]) 
45         return i-T[0];//如果j比子串T的长度大,说明全部匹配,返回主串中匹配的下标
46     else 
47         return 0;
48 }
49 
50 int main()
51 {
52     char s[MAXSIZE]="goodgoogle";
53     char t[MAXSIZE]="google";
54     char SS[MAXSIZE+1];
55     char TT[MAXSIZE+1];
56     StrAssign(SS,s);
57     StrAssign(TT,t);//字符串复制,第一位记录字符串长度,第二位开始存放字符
58     cout<<(int)TT[0]<<endl;
59     cout<<(int)SS[0]<<endl;//确认复制后长度是否正确
60     cout<<Index(SS,TT,1)<<endl;//调用朴素模式匹配算法,输出5
61 }

运行结果:

时间复杂度分析:(n为主串长度,m为子串长度)

1、最好的情况

一开始就匹配成功,O(1);

abcdefgoogle中找google,O(n+m);

根据等概率原则,平均是O((n+m)/2)次查找,时间复杂度是O(n+m)。

2、最坏的情况

每次不成功匹配都发生在子串的最后一个字符,举个例子:

主串0000001,长度7,子串001,长度3。

主串第1 2 3 4 5个位置都判断3次。

因此时间复杂度是O((n-m+1)*m)。

看出朴素模式匹配算法低效,看下面KMP模式匹配算法。

二、KMP模式匹配

对《大话数据结构》P135~P142—KMP模式匹配算法,进行了自己的理解并完善了代码。

在朴素模板匹配的基础上有两点可以优化:

1、朴素模板匹配算法中,主串的i值不断回溯。其实i是不需要来回变化的,考虑只回溯j值。比如:

看出i是在不停地来回回溯,其实这是不需要的。i只前进不倒退,j回溯就可以了。

2、j回溯也不是从头开始回溯,可以通过观察子串T结构中是否有重复。比如:

i=6,j=6,的时候,发现c不等于x。这时候j只需要回到j=3就可以。为什么?

因为abcabx,next[6]=3。即x前面的串前缀和后缀两个是一样的(这里是ab=ab)。既然j能到6,前面子串和串1~5是对应的。既然子串的1,2和4,5一样。那么串的4,5和子串的1,2也一样。所以直接比较i=6,和j=3。发现不一样。那么只能j再回到1。

把子串T各个位置的j值的变化定义为数组next,next是比较好得到的。即每次j在某个位置需要回溯的时候,回到哪个位置,由next数组告知。

举个例子:

代码和解释如下(VS2012测试通过):

 1 #include <iostream>
 2 #include <string.h>
 3 using namespace std;
 4 
 5 #define OK 1
 6 #define ERROR 0
 7 #define MAXSIZE 13
 8 
 9 typedef int Status;
10 
11 //把pchar指向的字符串内容复制给T指向的字符串
12 Status StrAssign(char *T,char *pchar)
13 { 
14     int i;
15     if(strlen(pchar)>MAXSIZE)//如果要拷贝的内容大于数组能存放的内容
16         return ERROR;
17     else
18     {
19         T[0]=strlen(pchar);//T[0]放串的长度
20         for(i=1;i<=T[0];i++)//循环T[0]次,即拷贝每个字符
21             T[i]=*(pchar+i-1);//-1是因为要从chars的第一个字符char[0]开始拷贝
22         return OK;
23     }
24 }
25 
26 //计算子串T的next数组
27 void get_next(char *T,int *next)
28 {
29     int i=1,j=0;
30     next[1]=0;
31     while(i<T[0])//T[0]表示子串T的长度
32     {
33         if(j==0||T[i]==T[j])//T[i]是当前比较的串后缀的最后一个字符,T[j]是当前比较的串前缀的最后一个字符
34         {
35             ++i;
36             ++j;
37             next[i]=j;
38         }
39         else
40             j=next[j];//若字符不相同,前缀的下标回溯
41     }
42 }
43 
44 //KMP模式匹配法
45 int Index_KMP(char *S,char *T, int pos) 
46 {
47     int i = pos;//从主串S的pos位置开始匹配
48     int j = 1;//j用于子串T中当前位置下标值
49     int next[MAXSIZE];
50     get_next(T,next);//对子串T分析,得到next数组
51     while (i <= S[0] && j <= T[0])//若i小于S的长度并且j小于T的长度,循环继续
52     {
53         if (j==0||S[i] == T[j])//两字母相等则继续,增加了j==0判断
54           {
55             ++i;
56             ++j; 
57           } 
58           else //指针后退重新开始匹配 
59           {  
60             j=next[j];//j退回合适的位置,i值不变
61           }      
62     }
63     if (j > T[0]) 
64         return i-T[0];//如果j比子串T的长度大,说明全部匹配,返回主串中匹配的下标
65     else 
66         return 0;
67 }
68 
69 int main()
70 {
71     char s[MAXSIZE]="abcababcabx";
72     char t[MAXSIZE]="abcabx";
73     char SS[MAXSIZE+1];
74     char TT[MAXSIZE+1];
75     StrAssign(SS,s);
76     StrAssign(TT,t);//字符串复制,第一位记录字符串长度,第二位开始存放字符
77     cout<<(int)TT[0]<<endl;
78     cout<<(int)SS[0]<<endl;//确认复制后长度是否正确
79     cout<<Index_KMP(SS,TT,1)<<endl;//调用KMP模式匹配法,输出6
80 }

运行结果:

时间复杂度分析:(n为主串长度,m为子串长度)

KMP算法关键是去掉了i回溯的部分。

对于get_next函数,因为只涉及到简单的循环,寻找每个j下标的next值,时间复杂度为O(m);

由于i不回溯,index_KMP算法复杂度就是O(n);

因此算法复杂度为O(n+m),相比朴素匹配算法O((n-m+1)*m),是要好一些。

KMP算法仅当模式与主串之间存在许多部分匹配的情况下才体现出优势。

三、改进的KMP模式匹配算法

2,3,4,5步骤都是多余的。由于子串T中2,3,4,5位置都与首位a相等。可以用首位的next[1]取代与它相等的后续字符next[j]的值。直接跳过j的回溯,直接进行到i+1。重点是求取代next[i]的nextval[i]数组。

比如:

举个例子:

 代码和解释如下(VS2012测试通过):

 1 #include <iostream>
 2 #include <string.h>
 3 using namespace std;
 4 
 5 #define OK 1
 6 #define ERROR 0
 7 #define MAXSIZE 13
 8 
 9 typedef int Status;
10 
11 //把pchar指向的字符串内容复制给T指向的字符串
12 Status StrAssign(char *T,char *pchar)
13 {
14     int i;
15     if(strlen(pchar)>MAXSIZE)//如果要拷贝的内容大于数组能存放的内容
16         return ERROR;
17     else
18     {
19         T[0]=strlen(pchar);//T[0]放串的长度
20         for(i=1;i<=T[0];i++)//循环T[0]次,即拷贝每个字符
21             T[i]=*(pchar+i-1);//-1是因为要从chars的第一个字符char[0]开始拷贝
22         return OK;
23     }
24 }
25 
26 //修正next数组,得到nextval数组
27 void get_nextval(char *T,int *nextval)
28 {
29     int i=1,j=0;
30     nextval[1]=0;
31     while(i<T[0])//T[0]表示子串T的长度
32     {
33         if(j==0||T[i]==T[j])//T[i]是当前比较的串后缀的最后一个字符,T[j]是当前比较的串前缀的最后一个字符
34         {
35             ++i;
36             ++j;
37             if(T[i]!=T[j])
38                 nextval[i]=j;
39             else
40                 nextval[i]=nextval[j];//替换next[i]=j;
41         }
42         else
43             j=nextval[j];//若字符不相同,前缀的下标回溯
44     }
45 }
46 
47 //改进的KMP模式匹配法
48 int Index_KMP(char *S,char *T, int pos)
49 {
50     int i = pos;//从主串S的pos位置开始匹配
51     int j = 1;//j用于子串T中当前位置下标值
52     int nextval[MAXSIZE];
53     get_nextval(T,nextval);//对子串T分析,得到next数组
54     while (i <= S[0] && j <= T[0])//若i小于S的长度并且j小于T的长度,循环继续
55     {
56         if (j==0||S[i] == T[j])//两字母相等则继续,增加了j==0判断
57         {
58             ++i;
59             ++j;
60         }
61         else //指针后退重新开始匹配
62         {
63             j=nextval[j];//j退回合适的位置,i值不变
64         }
65     }
66     if (j > T[0])
67         return i-T[0];//如果j比子串T的长度大,说明全部匹配,返回主串中匹配的下标
68     else
69         return 0;
70 }
71 
72 int main()
73 {
74     char s[MAXSIZE]="abcababcabx";
75     char t[MAXSIZE]="abcabx";
76     char SS[MAXSIZE+1];
77     char TT[MAXSIZE+1];
78     StrAssign(SS,s);
79     StrAssign(TT,t);//字符串复制,第一位记录字符串长度,第二位开始存放字符
80     cout<<(int)TT[0]<<endl;
81     cout<<(int)SS[0]<<endl;//确认复制后长度是否正确
82     cout<<Index_KMP(SS,TT,1)<<endl;//调用改进的KMP模式匹配法,输出6
83 }

运行结果:

posted @ 2016-04-22 11:05  Pearl_zju  阅读(321)  评论(0编辑  收藏  举报