字符串匹配算法

问题大致含义:给出两个字符串s1、s2,判断s2是否为s1的子串。


普通匹配算法:

时间复杂度:O(mn),m,n是两个串的长度。

 1 /* 字符串下标始于 0 */
 2 int NaiveStringSearch(string S, string P)
 3 {
 4     int i = 0;    // S 的下标
 5     int j = 0;    // P 的下标
 6     int s_len = S.size();
 7     int p_len = P.size();
 8 
 9     while (i < s_len && j < p_len)
10     {
11         if (S[i] == P[j])  // 若相等,都前进一步
12         {
13             i++;
14             j++;
15         }
16         else               // 不相等
17         {
18             i = i - j + 1;
19             j = 0;
20         }
21     }
22 
23     if (j == p_len)        // 匹配成功
24         return i - j;
25 
26     return -1;
27 }

KMP算法:

算法核心:当算法匹配成功的时候与普通的字符串匹配算法并没有太大的不同,都是比较下一个元素;但是在匹配失败的时候,利用某些信息来减少一些不必要的匹配。

next数组:

  • next数组是KMP的核心,当匹配失败时,通过next数组的意义能够减少不必要的匹配。
  • next数组的含义:对于一个字符串S,其长度为len(S),那么next的下标范围为:0~len(S)-1,对于i∈[0,len(S)-1],next[i]:使子串S[0:i]中形成前缀S[0:k]后缀S[i-k:i]相同最大k值(k一定小于i,当没有一个能匹配,取-1)。
  • next数组的意义:对于S,给出了所有S[0:i]的子串最长的前缀与后缀相同的位置。(字符串匹配的目的是实现所有的相同,所有相同必定是部分相同的子集,通过next数组跳入下一个可能匹配的位置)            (核心:等价代换;部分与整体的关系)

用一个例子来解释next[i]:   S="ababaab"  len(S)=7              (下面所用的符号与上面说明对应)

i 0 1 2 3 4 5 6
next[i] -1 -1 0 1 2 0 -1
  • i=0时,默认为-1;(只有一个元素,S[0])
  • i=1时,S[0:1]="ab",next[i]=-1,因为k=0时,S[0:0]="a",S[1:1]="b",不匹配,k必须小于i。
  • i=2时,S[0:2]="aba",next[i]=0,因为k=0时,S[0:0]="a",S[2:2]=“a”,匹配;    k=1时,S[0:1]="ab",S[1:2]="ba",不匹配。
  • i=3时,S[0:3]="abab",next[i]=1。 
  • i=4,5,6::略

KMP算法:

  • 注意:在匹配失败的时候如何利用next数组,以及i,j如何变化。

S1="abababaabc"    S2="ababaab",判断S2是否为S1的子串。

  • 建立S2的next数组;
  • 用i指向S1欲匹配的字符,用j指向S2已匹配的字符;i=0,j=-1; 
  • 用S1[i]与S2[j+1]进行匹配:
    • i∈[0,4],是匹配成功的,这时候只需要i++,j++; 最后到达i=5,j=4;(匹配成功情况
    • 因为S1[i]="b",S2[j+1]="a",因此不匹配;    关键点来了,那么用什么元素进行再次比较呢? (图1) (匹配失败情况)     此时S1[0:4]=S2[0:4],  根据next数组,next[j]=2(说明S2[0:2]=S2[2:4] ) S2[2:4]=S1[2:4],   经过等价代换   ,可以发现:S2[0:2]=S1[2:4];    同时令j=next[j],在此时再比较S1[i]与S2[j+1],看是否相等(i=5,j=2);这时候发现相等了,因此按照成功时候进行匹配。

                                        图1

  

                                        图2

算法步骤综述:(用数学关系完整地阐述一遍)  S1:被匹配串, S2:匹配串

  • 用i指向S1欲匹配的字符,用j指向S2已匹配的字符;i=0,j=-1;     建立S2的next数组。
  • 循环结束条件:当j=len(S2)-1 or i=len(S1)  ;   如果j=len(S2)-1,那么匹配成功;  否则,匹配失败
  • 循环过程:
    • 如果S1[i]=S2[j+1],那么匹配成功,i++,j++;
    • 如果S1[i]!=S2[j+1],那么匹配失败,j=next[j],循环判断S1[i]与S2[j+1]的关系:   如果j=-1,并且匹配失败,那么i++;继续沿着该循环过程循环。

 

next数组的实现:

  • 很容易想到,next数组也能够通过暴力求解:当然这个效率就很低了。
1 next[0]=-1;
2 
3 for(int i=1;i<len(S);i++){
4     int j;
5     for(j=i-1;j>=0;j--){
6         //判断 子串[0:j]和[len(S)-1-j,len(S)-1]是否相等;当相等的时候,break;
7     }
8     next[i]=j;
9 }
i 0 1 2 3 4 5 6
next[i] -1 -1 0 1 2 0 -1

  • 改进实现:对于当前 i,考虑next[i]与next[0]~next[i-1]的关系:以S="ababaab"为例子
    • 初始值:next[0]=-1,i=1。
    • 考虑i=4和i=5的实现:i=4能够体现出匹配成功的情况,i=5能够体现出匹配失败的情况 (next表见上方)
      • 对于i=4,next[i-1]=1;  这时候考虑S[i]="a"和S[next[i-1]+1]="a"的关系,相等,那么next[i]=next[i-1]+1;
      • 对于i=5,next[i-1]=2; 这时候考虑S[i]="a"和S[next[i-1]+1]="b"的关系,不相等,那么匹配失败;       注意此刻条件,S[0:next[i-1]]和S[i-next[i-1]-1:i-1]是相同的,只是S[i]="a"和S[next[i-1]+1]不同,对于串的匹配,所有匹配必是部分匹配的子集,那么考虑前缀S[0:next[i-1]],求出得到next[next[i-1]]=0,判断S[0+1]="b"与S[i]="a"的关系,发现还是不相同;  那么继续失败的模式,next[next[next[i-1]]]=-1,判断S[-1+1]="a"与S[i]="a"的关系,发现相同,那么令next[i]=next[0]+1;

 

                                 图3)i=4时,S[i]与S[next[i-1]+1]        

   

图4) 第一次匹配失败   S[5]与S[next[4]+1]

图5)第二次匹配失败 S[5]与S[1]

图6) S[5]与S[0]匹配成功 (如果这次再匹配失败,那么i++,next[5]=-1)

  • 究其本质:发现next数组的实现过程其实是对于每一个S[0:i]和自身某些前缀的KMP匹配。对于S[0:i],已知next[0]~next[i-1],next[0]~next[i-1]提供了前缀和后缀的一致信息,我们只要保证S[i]和S[next[i-1]+1]相等就行。

 1 int* getNext(char* S,int length){
 2     int* next=(int*)malloc(sizeof(int)*length);
 3     next[0]=-1;
 4     // next[i]        
 5     for(int i=1;i<length;i++){
 6         int j=next[i-1];
 7         while(S[i]!=S[j+1]){
 8              if(j==-1){   // 无匹配项
 9                  break;
10              }
11             else{
12                  j=next[j];   
13             }
14         }    
15         if(S[i]==S[j+1]){    
16             j++;
17         }
18         next[i]=j;
19     }
20   return next;
21 }

KMP算法的实现:

bool KMP(char* S1,char* S2){
    int n=strlen(S1),m=strlen(S2);
    getNext(S2,m); // 获得S2的next数组
    int j=-1;  // 记录S2已匹配串位置
    int i=0;  // 记录S1欲匹配位置
    while(i<n && j<m-1){
        // 匹配成功
        if(S1[i]==S2[j+1]){
            i++;
            j++;
        }
        else{
            // 匹配失败,并且j=-1
            if(j==-1){
                i++;
            }
            else{
                j=next[j];
            }
        }
    }
    if(j==m-1){
        return true;
    }
    else{
        return false;
    }
}

 

原文地址:https://www.cnblogs.com/yy-1046741080/p/11767457.html