代码改变世界

字符串匹配算法

2011-06-09 19:27  Firefly727  阅读(626)  评论(0编辑  收藏  举报
[概述]
  字符串匹配算法是计算机程序中比较常见的算法,它是指现在有一个目标串,给你一个模式串,要求你在该目标串中找出模式串出现的地方。经过无数学者这么多年的研究,现在有很多现成的字符串匹配算法了,各有各的特色。下面对它们进行一下总结。
  字符串匹配算法由两部分组成:预处理阶段和匹配阶段。
  假设    目标串为T[1…n]         模式串为P[1…m]    T和P中记录都是在有限集合∑中,如记录是十进制数字时,∑={0,1,2,3,4,5,6,7,8,9,},记录是字符时,∑={a,b,c……,x,y,z}. 
[各种匹配算法]
  ①朴素匹配算法(我称之为"笨"方法)
    核心思想:既然给我一个目标串T,一个模式串P,要在T中找出P出现的所有位置。那我就找,从第i(0,1,2,3……)个元素开始进行匹配,如果匹配成功则输出i,否则从i+1开始从头匹配。
    代码
1 void NaiveStrMatch(char T[],char P[]) {
2 int i,j;
3 int len_t,len_p,len;
4 len_t = strlen(T);
5 len_p = strlen(P);
6 len = len_t - len_p;
7 for(i=0; i<len; i++) {
8 for(j=0; j<len_p; j++) {
9 if(T[i+j] != P[j]) break;
10 }
11 if(j == len_p) printf("the match position:%d-%d\n",i,(i+j-1));
12 }
13
14 }

  时间复杂度:预处理时间(0) + 匹配时间 O((n-m+1)m) = O((n-m+1)m)  空间复杂度 O(n+m+5)
  该算法比较简单,容易理解和实现。但是它的时间复杂度比较高。
  ②Rabin-Karp算法
  核心思想:将模式串P表示成为一个值,这样每次进行串匹配时,只需要比较这个值就可以了,而不需要对m个字符串进行m次比较。对于值和模式串P不同的可以直接跳过,而对于值相同则要进行进一步的一一比较。
  比如:T="238203988349809353045839885034"   P="3988"
  计算对应的值  T‘ = [2382][3820][8203][2039][0398][3988][9883][8834]……[5034]  P'=[3988]
  在计算一个数时,可以借助前一个数,比如现在我们计算到了第二个数,需要第三个数,其计算如下
  8203 = (3820-3[旧的高位]*1000)*10[向右移位] + 3[新的低位]
  对于记录是字符的时候,其进制可以选为256
  这种方法会产生一个新的问题,即P的计算值溢出,可以通过取模来解决这个问题,同时这个问题也导致了在找到对应的值的时候,需要进行进一步的比较,因为会有伪命中。
  代码:
1 void RabinKarpMatch(int T[],int P[], int q) {
2 //这里对整型进行举例,字符串的可以类推。
3 //q是我们取模时要用到的素数。
4   int len_t,len_p, len,i,j;
5 int pCount=0, tCount=0,h,temp;
6
7 len_t = sizeof(T)/sizeof(int);
8 len_p = sizeof(P)/sizeof(int);
9 len = len_t - len_p;
10 h = pow(10, len_p)%q;
11 for(i=0; i<len_p; i++) {
12 //预处理,计算出数组P对应的值,计算T第一个m位记录对应的值
13   pCount = (pCount * 10 + P[i])%q;
14 tCount = (tCount * 10 + T[i])%q;
15 }
16
17 for(i=0; i<len; i++) {
18 if(pCount == tCount) {
19 //值相等的时候,进行进一步的比较
20   temp = i+len_p;
21 for(j=i; j<temp; j++) {
22 if(T[j] != P[j-len_p]) break;
23 }
24 if(j == temp) printf("the match position:%d-%d\n",i,(i+len_p-1));
25 j = 0;
26 temp =0;
27 }
28 tCount = (10*(tCount - T[i]*h) + T[i+len_p])%q;
29 }
30
31 }

  时间复杂度:预处理时间(O(m)) + 匹配时间(km) = O((k+1)*m)  其中k是指目标串中出现模式串的个数。
  该算法采用的方法相比朴素字符处匹配算法来说,还是能够提高很多效率,但是它的最坏时间复杂度仍然为O((n-m+1)m);即所有T的计算值都产生命中或伪命中。当目标串中模式串出现比较少时,该算法比较适用。
③有限自动机匹配
  有限自动机定义:一个有限自动机M就是一个5元组(Q,q0,A,∑,δ)
      Q:状态的有限集合;
      q0:属于Q,初始状态
      A:包含于Q,可接受状态[在字符串匹配里面就是找到一个匹配字符串]
      ∑:有限的输入记录集
      δ:一个从 Q*∑ 到 Q的函数,称为状态转移函数。 其中的 Q*∑ 是指,在Q中的某个状态下 接受到∑的某个记录。
  核心思想:对于T和P来说,它里面的记录都属于同一个∑,而对于P来说,在刚开始进行匹配时,它的状态为q0,每遇到一个匹配的记录就将状态转入下一个状态,当所有的记录都匹配时,到达了接受状态A,所以对于任何一个模式串P存在一个字符串匹配自动机。主要是要去定义状态机的自动转移函数。
      δ(x) = max(k|k是x的后缀P的最长前缀的长度)  
      比如 P=“ab” ---δ(ε)=0; δ("bbaa") = 1; δ("bbab") = 2;δ("bbaba") = 1;δ("bbababb") = 0
  假设现在P=“abcabab”    T = “abcabcabababc”
  P的状态转换表为
            输入         
 状态  a    b     c  P
  0       1      0      0       a   
  1     1   2   0    b
  2    1   0   3    c
  3    4   0   0    a
  4    1   5   0    b
  5    6   0   3    a
  6    1   7   0    b
  7    1   0   3
                                      i         1  2  3  4  5  6  7  8  9  10  11  12  13
             T[i]       a  b  c  a  b  c   a  b  a   b    a    b    c
            Φ(T[i])  0 1  2  3  4  5  3  4  5  6   7  1    2    3  
代码:
1 int FindMaxPreLen(char temp[], int len_temp, char P[], int len_p) {
2 //找出temp的后缀P的最长前缀的长度。len_temp 和len_p分别是两个数组的长度
3   int i,k=0;
4 char *arr1=NULL;
5 for(i=0; i<len_temp) {
6 if(i) {
7 if(temp[len_temp-1] == P[0]) k=1;
8 }else {
9 arr1 = &temp[len_temp-1-i];
10 if(!strncmp(arr1, P, (i+1))) k += 1;
11 }
12 }
13 return k;
14 }
15  void ComputeStateArray(char P[], char set[], int len, int StateArray[][]) {
16 //计算模式串P的自动机状态转变数组,set为记录集。 len为set的长度。
17   int i,j,len_p;
18 char *temp;
19
20 len_p = sizeof(P)/sizeof(char);
21 temp = (char*)malloc(sizeof(char) * (len_p));
22 if(!temp) {
23 printf("3、error in malloc function!\n");
24 exit(1);
25 }
26 for(i=0; i<=len_p; i++) {
27 strncpy(temp, P, i);
28 for(j=0; j<len; j++) {
29 temp[i] = set[P];
30 StateArray[i][j] = FindMaxPreLen(temp,i+1,P,len_p);
31 }
32 }
33 free(temp);
34 }
35  int StateChange(int curState, char curInput,int StateArray[][], char set[]) {
36 // StateArray[][] 状态的集合。
37 //该函数接收一个状态和一个输入字符,返回下一个状态
38   int i,j, len_s;
39 len_s = sizeof(set)/sizeof(char);
40 for(i=0; i<len_s; i++) {
41 if(set[i] == curInput) break;
42 }
43 if(i == len_s) {
44 printf("there is no such char %c",curInput);
45 exit(1);
46 }
47 return StateArray[curState][i];
48 }
49  int IsExist(char p[], char c, int len) {
50 // 检查P中是否存在字符c,是的时候返回1,否则返回0;
51 // len 为p中元素的个数
52 int i,flag =0;
53 for(i=0; i<len; i++) {
54 if(p[i] == c) {
55 flag=1;
56 break;
57 }
58 }
59 return flag;
60 }
61 void FiniteAutomationMatch(char T[], char P[]) {
62 int len_t,len_p,q,i,count=0;
63 int ** StateArray;
64 char *pt=NULL;
65 len_t = sizeof(T)/sizeof(char);
66 len_p = sizeof(P)/sizeof(char);
67 for(i=0; i<len_t; i++) {
68 if(i){
69 //这里假设了模式串P中出现的字符都包含在目标串T中
70 if(!IsExist(pt,T[i],count)) {
71 //不存在时要将它加入其中
72 pt = (char*)realloc(pt,sizeof(char)*(count+1));
73 if(!pt) {
74 printf("1、error in realloc function!\n");
75 exit(1);
76 }
77 pt[count] = T[i];
78 count++;
79 }
80 }else {
81 pt=(char*)malloc(sizeof(char));
82 if(!pt) {
83 printf("1、error in malloc function!\n");
84 exit(1);
85 }
86 pt[count] = T[i];
87 count++;
88 }
89 }
90 StateArray = (int**)malloc(sizeof(int*) * (len_p+1));
91 if(!StateArray) {
92 printf("2、error in malloc function!\n");
93 exit(1);
94 }
95 for(i=0; i<=len_p; i++) {
96 StateArray[i] = (int*)malloc( sizeof(int) * count );
97 if(!StateArray[i]) {
98 printf("3、error in malloc function!\n");
99 exit(1);
100 }
101 }
102 ComputeStateArray(P, pt, count, StateArray);
103 q=0; //q为初始状态
104
105 for(i=0; i<len_t; i++) {
106 q = StateChange(q,T[i],StateArray);
107 if(q == len_t) {
108 printf("the match position:%d-%d\n",i,(i+len_p-1));
109 }
110 }
111 free(pt);
112 free(StateArray);
113 }
时间复杂度: 预处理时间 O(m |∑|) + 匹配时间 O(n)    辅助空间就用到了很多了,主要是在计算状态数组的时候。
概算法适合于记录集合中元素比较少。模式串P比较短的情况,否则耗费在预处理阶段的时间将会很大。
④Knuth-Morris-Pratt (KMP算法)
  核心思想:当模式串P中的P[j]和目标串的T[i]不等时, 不是从新从[i-j+1]进行新的匹配,因为现在我有一些可用的信息,如T[i-j,…,i-1] == P[1,…,j-1]; 如果现在我知道在有这么多记录相同的情况,知道下一步移动的距离,就可以直接滑动到那个位置。比如假设现在应该让模式串P中的第k个元素桶T中第i个元素比较。那么有
          P[1,2,…,k-1]   =   T[i-k+1,i-k+2,…,i-1]       (1)
  而由T[i-j,…,i-1] == P[1,…,j-1]  有
          T[i-k+1,i-k+2,…,i-1]  =  P[j-k+1,j-k+2,…,j-1]     (2)
  从而有
          P[1,2,…,k-1]     =  P[j-k+1,j-k+2,…,j-1];               (3)
现在就是要找出第i元素不匹配时,其对应的最大值k。可以将它定义成一个数组,其计算如下
  当j=1时;即第一个元素就不匹配,此时next[j]  = 0,直接移到下一个元素进行匹配。
  当j>1且(3)中k>1 时,next[j] = Max{k | 1<k<j 且P[1,2,…,k-1] = P[j-k+1,j-k+2,…,j-1]}
  当J>1且(3)中k=1时,next[j] = 1;
时间复杂度 :预处理时间O(m) + 匹配时间 O(n)
⑤Boyer-Moore算法[参考文献1]
  BM算法有几个要点
  1.从右向左进行比较,也就是比较次序为P[m],P[m-1]...2.当某趟比较中出现不匹配的字符时,BM采用两条启发式规则计算模式串移动的距离,即bad character shift rule坏字符和good suffix shift rule好后缀规则。 
  坏字符与好后缀的定义
    Index:   0 1 2 3 4 5 6 7 8 9 10 11
    Text:    k s e a b c d e f a b c a 
    Pattern:   d e  c a b c            
  从右往左进行匹配,第一个mismatch的字符c就是坏字符,后面已经成功匹配的字串abc就是好后缀。

  1) 坏字符规则   P中的某个字符与T中的某个字符不相同时使用坏字符规则右移模式串P,P右移的距离可以通过delta1函数计算出来。delta1函数的定义如下:     delta1(x) = 如果字符x在P中未出现,则为 m(模式长度); 否则 m-k, k是最大的整数使得P[k]=字符x,用数学表达式则为:m - max{k|P[k] = x, 1 <= k <= m};解释一下,如果字符x在模式P中没有出现,那么从字符x开始的m个文本显然不可能与P匹配成功,直接全部跳过该区域即可。如果x在模式P中出现,则以该字符进行对齐。

  2) 好后缀规则   P中的某一子串P[j-s+1..m-s]与已比较部分P[j+1..m]相同,可让P右移s位。    delta2的定义如下:   
delta2(j) = {s | P[j+1..m] == P[j-s+1..m-s] ) && ( P[j] ≠ P[j-s] ) (j>s) }    

⑥ZZL算法

  核心思想:首先在主串T中查找模式串P的首字母,每找到一个则将它的位置存储,然后依次提取这些位置,从这些位置开始继续匹配模式串P。对于频繁使用的要匹配的主串和模式串来说,由于预先保存了模式串在主串中的所有存储位置,所以匹配速度会非常快。[不同于现有的字符串匹配算法,现有的匹配算法不是按照模式串从左到右就是从右到左进行直接比较的顺序匹配]

  代码:

 

1 void FindFirstChar(char T[], int len, char Head,int *arr) {
2 //arr 中存放主串中,目标串首字符出现的次数。而arr[0]存放arr数组
3 //元素的个数
4 int i;
5 arr = (int*)malloc(sizeof(int));
6 if(!arr) {
7 printf("1、error in malloc function!");
8 exit(1);
9 }
10 arr[0]=0;
11 for(i=0; i<len; i++) {
12 if(T[i] == Head) {
13 arr[0]++;
14 arr = (int*)realloc(arr,sizeof(int)*(arr[0]+1));
15 arr[arr[0]] = i;
16 }
17 }
18
19 }
20
21 void ZZLMatch(char T[], char P[]) {
22 int i, j, len_t, len_p, len, *arr=NULL;
23 len_t = strlen(T);
24 len_p = strlen(P);
25 len = len_t - len_p;
26 FindFirstChar(T,len,P[0],arr);
27 for(i=1; i<=arr[0]; i++) {
28 for(j=0; j<len_p; j++) {
29 if( T[arr[i]+j] != P[j]) break;
30 }
31 if(j == len_p) printf("the match position:%d-%d\n",i,(i+j-1)
32 }
33 free(arr);
34 }

时间复杂度: 预处理时间 O(n-m) + 匹配时间 O(km)  其中k为模式串P的首字符在目标串中出现的次数。

空间复杂度:O(n+m+k+6)

ZZL缺点分析
     观察ZZL算法,如果模式串首字母在主串中出现次数很多,那么ZZL算法记录的匹配点将会增加,时间复杂度也随之增加。让我们先来考察下面的匹配场景:
S = tom orrow, m any and any m anufactories will close.
T = manufactories
     那么ZZL算法在预处理S时会找到4个开始匹配点,然而事实上,只有在最后一个匹配点上TS才匹配成功。这样仍可以进行改进。

⑦ZZL算法的改进
  核心思想:在模式串P中第二次出现P[0]的位置假设为pos,那么short = (pos+1)就是ZZL首字母最短路径,如果P[0]在P中只出现一次,那么pos等于m-1,那么short = m就是ZZL首字母最短路径;如果在目标串T中出现模式串P首字符的任何两个连续位置之间的距离小于short,则可以排除前一个位置。
  代码
1 void FindFirstChar(char T[], int len, char Head,int short, int *arr) {
2 //arr 中存放主串中,目标串首字符出现的次数。而arr[0]存放arr数组
3 //元素的个数
4 int i;
5 arr = (int*)malloc(sizeof(int));
6 if(!arr) {
7 printf("1、error in malloc function!");
8 exit(1);
9 }
10 arr[0]=0;
11 for(i=0; i<len; i++) {
12 if(T[i] == Head && (i-arr[arr[0]] > short)) {
13 arr[0]++;
14 arr = (int*)realloc(arr,sizeof(int)*(arr[0]+1));
15 arr[arr[0]] = i;
16 }else {
17 arr[arr[0]] = i;
18 }
19 }
20
21 }
22
23 void ImproveZZLMatch(char T[], char P[]) {
24 int i, j, len_t, len_p, len, *arr=NULL, short;
25 len_t = strlen(T);
26 len_p = strlen(P);
27 len = len_t - len_p;
28 for(i=1; i<len_p; i++) {
29 if(P[i] == P[0]) {
30 short = i;
31 break;
32 }
33 }
34 FindFirstChar(T,len,P[0],short,arr);
35 for(i=1; i<=arr[0]; i++) {
36 for(j=0; j<len_p; j++) {
37 if( T[arr[i]+j] != P[j]) break;
38 }
39 if(j == len_p) printf("the match position:%d-%d\n",i,(i+j-1)
40 }
41 free(arr);
42 }

⑧sunday算法
  核心思想:在匹配过程中,模式串并不被要求一定要按从左向右进行比较还是从右向左进行比较,它在发现不匹配时,算法能跳过尽可能多的字符以进行下一步的匹配,从而提高匹配效率。

  假设在发生不匹配时S[i]≠T[j]1≤i≤N1≤j≤M。此时还未匹配的部分为u,并假设字符串u的长度为L。如图1。明显的,S[L+i+1]肯定要参加下一轮的匹配,并且T[M]至少要移动到这个位置(即模式串T至少向右移动一个字符的位置)。                 

                        

                        图1  Sunday算法不匹配的情况

    分如下两种情况:

    (1) S[L+i+1]在模式串T中没有出现。这个时候模式串T[0]移动到S[L+i+1]之后的字符的位置。如图2

                      

                        图2  Sunday算法移动的第1种情况

    (2)S[L+i+1]在模式串T中出现。这里S[L+i+1]从模式串T的右侧,即按T[M-1]T[M-2]…T[0]的次序查找。如果发现S[L+i+1]T中的某个字符相同,则记下这个位置,记为k1≤k≤M,且T[k]=S[L+i+1]。此时,应该把模式串T向右移动M-k个字符的位置,即移动到T[k]S[L+i+1]对齐的位置。如图3

                      

                        图3  Sunday算法移动的第2种情况

     依次类推,如果完全匹配了,则匹配成功;否则,再进行下一轮的移动,直到主串S的最右端结束。对于短模式串的匹配问题,该算法执行速度较快。

代码

1 void sunday(const char *T,const char *P)
2 {
3 int i,j,pos=0;
4 int len_t,len_p;
5 int next[26]={0};
6
7 len_t=strlen(T);
8 len_p=strlen(P);
9
10 for(j=0; j<26; ++j)
11 next[j] = len_p;
12
13 for(j=0; j<len_p; ++j)
14 next[P[j]-'a']=len_p-j;
15
16 while( pos < (len_t-len_p+1) )
17 {
18 i=pos;
19 for(j=0; j<len_p; ++j,++i)
20 {
21 if(T[i]!=P[j])
22 {
23 pos+=next[T[pos+len_p]-'a'];
24 break;
25 }
26 }
27 if(j==len_d)
28 printf("the match position:%d-%d\n",pos,(pos+j-1));
29 }
30 }

举个例子

假设   T  =  “abaabcabca”   len_t = 10

   P  =   “abcabc”        len_p = 6 

  一开始的时候next[0,1,2,…,25] = {6}   经过13行的for循环后变成了next[0,1,2] = {3,2,1} next[3,……25] ={6};由于我们的模式串P中只出现了字符a,b,c他们对应的位置分别为0,1,2; 而所以存储在这三个位置。

  在开始比较时,pos=0, 在i=j=2时不匹配,此时要进行pos位置的移动,那么再怎么样移动都至少要移一个位置,那么下一个位置也就是T中第7个元素a在下一个匹配过程中是一定要比较的,而a对应的next为3,将pos移动到3的位置去,再接着一轮匹配。next数组中存放的就是从右往左数某字符第一次出现的位置。

  该算法最坏情况下的时间复杂度为O(N*M)


[参考文献]
[2]算法导论
[3] 严蔚敏 数据结构