字符串模式匹配KMP算法


字符串模式匹配指的是,找出特定的模式串在一個較長的字符串中出現的位置。

  • 朴素的模式匹配算法

很直觀的可以寫出下面的代碼,來找出模式串在一個長字符串中出現的位置。

   1:  /*
   2:      朴素的模式匹配算法
   3:      功能:字符串的模式匹配
   4:      參數:
   5:          s:目標串
   6:          p:模式串
   7:          pos:開發匹配的位置
   8:      返回值:
   9:          匹配成功,返回模式串在目標串的其實位置
  10:          匹配不成功,返回-1
  11:  */
  12:  int match(const char * s ,const  char * p,int pos){
  13:      int i = pos ;
  14:      int j= 0 ;
  15:      while(s[i] != '\0' && p[j] != '\0') {
  16:          if(s[i] == p[j]) {
  17:               i ++ ;
  18:               j ++ ;
  19:          }else {
  20:              i = i - j + 1;
  21:              j = 0 ;
  22:          }
  23:      }
  24:   
  25:      if(p[j] == '\0')
  26:          return i - j ;
  27:      else 
  28:          return -1 ;
  29:  }

上面的代碼,s就是目標串,p是模式串,pos指定從s的什么位置開始匹配p。其實現思想也很簡單:

當s[i] == p[j]時,目標串和模式串的指針都向后移動一位,進行匹配。而當s[i] != p[j]時,即匹配不成功時,將目標串和模式串的指針同時回溯,j = 0 而目標串的指針i則回溯到這輪開始的下一個位置。

朴素的模式匹配的算法復雜度是O( (n-m+1) * m)  n為目標串的長度,m為模式串長度。

從其實現思想上可以很容易的看出,造成該算法低效的地方是在,匹配不成功時主串和模式串的指針回溯上。

有沒有一種算法,當模式串和主串的匹配不成功時,不用進行指針的回溯,直接進行下一輪的匹配?

  • KMP算法理解

在朴素的字符串模式匹配算法上,當遇到主串和模式串的字符不能匹配成功時,不論已經匹配了多少字符都要進行指針回溯,再開始下一輪的匹配。

這樣效率是十分的低下的。KMP算法,是在朴素的模式匹配算法的基礎上,實現了匹配不成功時,不對主串指針進行回溯,使模式匹配的時間復雜度

降低為:O(n + m)。

對KMP算法的理解,在網上查找了不少資料,也看了算法導論上的描述,一直是一知半解。有次閑暇之余,想像着將模式串、主串都看着是條直線,進行了下推導,才恍然大悟。

KMP算法的核心思想是,在s[i] 和 p[j]不匹配時,不對主串進行指針回溯,而是在模式串中p中尋找k,用s[i] 和 p[k]進行下一輪的匹配。

在這里,將主串 S 和模式串 P 都看成是一條直線,故而在S[i] 和 P[j] 匹配不成共時,有如下情形:

image 

圖1 s[i] 和 p[j] 匹配不成功

即是:p[1…j-1] == s[i-j+1,…,i-1].

p[j] 和 s[i] 不匹配,現在要在模式串p[1,…,j-1]確定一個位置k(1<= k < j-1),用p[k]和s[i]進行下一輪匹配,那么k必須要滿足以下條件:

p[1,..,k-1] == s[i-k+1, … , i-1] .

將模式串和主串都看着一條直線,那么就有下圖:

image

圖2  使用p[k]和s[i]進行下一輪匹配

由於 1<= k < j-1,那么將兩圖合並起來會有什么效果呢?

image

從上圖可以看出,當s[i]和p[j]匹配不成功時,假如能用p[k]和s[i]進行下一輪匹配,則有:

s[i-k+1], … , i-1] == p[j-k+1,…,j-1] == p[1,…,k-1] 。

就是說,當s[i] 和 p[j] 匹配不成功時,最對主串不進行指針回溯,而是用p[k]和s[i]進行匹配時,k必須滿足以下條件:

p[1,…,k-1] == p[j-k+1, … , j-1]。

  • KMP算法的實現

KMP算法的是對匹配的模式匹配算法的改進,在s[i]和p[j]匹配不成功時,不是對主串進行指針的回溯,而是在p[1,…,j-1]中,尋找一個p[k],

用s[i]和p[k]進行下一輪的匹配。其實現的最大問題就是如何的根據p[1,…,j-1]來求出p[k]。

在KMP算法的實現中,使用一個輔助數組next[],使用該數組保存p[j]匹配不成功時,要進行下一輪匹配的k的值.即是當s[i] 和 p[j]匹配不成功時,

用p[ next[j] ]來和s[i]進行下一輪匹配,k = next[j] .

對數組next[] 的求解,可以goolge到不少的方法,這里使用最簡單的遞推的方法:

首先假定next[0] = –1,那么當next[j] = k時,就有:p[0,…,j-1] == p[j-k+1,…,j-1]。

這時,若有p[k] = p[j] ,則p[0,….,k] = p[j-k+1,..,j-1,j],從而就有next[j+1] = next[j] + 1 = k +1 .

若p[k] != p[j] ,可以看着模式串對自身進行匹配的問題,即當匹配失敗的時候,k值如何確定,k = next [k] .

求數組next[ ]的實現如下:

/*
    KMP進行模式匹配的輔助函數
    模式串和主串匹配不成功時,下次和主串進行匹配的模式串的位置
*/
void continue_prefix_function(const char * p , int * next) {
    int j ;
    int k ;
    next[0] = -1 ;
    j = 0 ;
    k = -1 ;

    while(j < strlen(p) - 1) {
        if( k == -1 || p[k] == p[j]) {
            j ++ ;
            k ++ ;
            next[j] = k ;
        }else {
            k =next[k] ;
        }
    }
}

知道了當模式串和主串匹配不成功時,下一個和主串匹配的字符在模式串中的位置,在朴素的模式匹配的基礎上很容易的寫出KMP算法的代碼如下:

 

/*
    運用KMP算法的字符串模式匹配
    在主串和模式串匹配不成功時,不對主串指針進行回溯,
    例如用next[j],來指定下一次和主串進行匹配的模式串的位置
*/
int match_kmp(const char * s ,const char * p,int pos) {
    int next[11] ;
    int i = pos ;
    int j = 0 ;
    continue_prefix_function(p,next) ;
    while(s[i] != '\0' && p[j] != '\0') {
        if(s[i] == p[j]) {
            i ++ ;
            j ++ ;
        }else {
            if(next[j] == -1) {
                i ++ ;
                j = 0 ;
            }
            else {
                j = next[j] ;
            }
        }
    }
    if(p[j] == '\0')
        return i - j ;
    else
        return -1 ;
}
  • 總結

一直想寫篇文章總結下自己對KMP算法的理解,拖拉很久,終於算完成了。不過,寫篇文章真是不容易啊,花了將近2個小時,也不知道表達清楚木有。微笑


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM