KMP算法—最通俗易懂的講解!

引言

我們把尋找字符串A(模式串)在字符串B(主串)第一次完整地出現的位置,把這個過程叫做字符串匹配,例如下圖:

image
在這種模式匹配中,最粗暴簡單的方法:

  • 開始之前記個k=0作爲匹配失敗的次數,i作爲模式串比較字符的位置,j作爲主串比較字符的位置;
  • 匹配成功時,接續向下一個字符進行匹配,直到匹配模式串匹配完成;
  • 當發現匹配失敗時,模式串重新到首個字符,而主串則回溯到k+1的位置,k自加1;
    image

![image](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWcuYWxpY2RuLmNvbS9pbWdleHRyYS9pMi8yNTk2NTMzOTcyL08xQ04wMU5ZN1ZWeDFmRERqWGkya1R1XyEhMjU5NjUzMzk3Mi5wbmc?x-oss-process=image/format,png)

image

這種方法是最簡單的,但同時也是最低效的:因爲在匹配的過程中,需要同時回溯i,j兩個參數;但是這種回溯大部分都是多餘的;爲了解決這種低效,1977由Donald Knuth、Vaughan Pratt、James H. Morris三人於年聯合了一個字符串模式匹配算法,Knuth-Morris-Pratt 字符串查找算法,簡稱爲 “KMP算法”。

代碼實現:
int index(Str str,Str substr)
{
    int i=1,j=1,k=i;
    while(i <= str.length && j<= substr.length)
    {
        if (str.ch[i]==substr[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j = 1;
            i =++k; #匹配失敗; 
        }   
    }
    if(j > substr.length)
    {
        return k;
    }
    else
    {
        return 0;
    }
}

KMP算法的匹配流程

假設主串S匹配到j的位置,模式串P匹配到i的位置,還沒匹配結束,繼續匹配下去會出現下面兩種情況:

  • 1,當P[i] == S[j]時,則繼續匹配 i++,j++;
  • 2,當P[i] != S[j]時,則 i =next[i],j++,此時等價於模式串P向右移動了 i -next[i]個單位;

以上只是帶大家初步認識了KMP算法的基本匹配思想,下面則需要進行深入地瞭解一下next數組,以及對匹配思想的理解;

第一步,字符串相同的前後綴元素

在理解next數組之前,需要直到next數組是怎麼求出來的,首先我們看一下下面的這張圖

image

圖中我們瞭解到幾個信息:

  • S爲主串;P爲模式串;
  • 圖中的S[i-k] — S[i]部分與P中的P[j-k]到P[j](模式串P的前面)部分相同,同時也與P[0]到P[k](模式串P的前面)相同;
  • 模式串向後移動了j-next[j]個單位;

從以上幾部分信息,可以這樣理解:

  • 在P未移動之前之前有部分連續字符串與S已經匹配成功了,同時P的前面一小部分也能匹配成功,所以直接可以把P的首個字符移動到與後面字符串重複地方的開始字符,也就是P[j-k];


  • 因爲只是部分連續,所以在移動的過程捕獲出現字符串匹配成功的可能。

上面重複的部分就是模式串的相同前後綴的最大長度,也就等於next函數,而移動的距離剛好是 j(匹配失敗的位置) - next[j](前面字符串前後綴重複的最大長度)

字符串重複前後綴的最大長度也就是字符串前面的連續字符(從第一個開始)有多少個與後面的相同,自身與自身不算同一個。如下圖:
image
也就能求出next數組,只不過是關於後一字符的;

第二步,已知next[1]....next[j],怎樣求得next[j+1]

在第一步中,已經分析過了,next數組的值與前後綴的最大長度相關,而關於最大長度得出的next值 是後一字符的next數組的值;

這裏,有一種比較權威的求next[j+1]的方法(這裏是以第一個字符未p[0]爲依據的):

  • next[0] = -1,因爲第一個字符比較特殊;
  • 若p[k] == p[j],則next[j+1] = next[j]+1 =k+1;
  • 若p[k ] ≠ p[j],如果此時p[next[k]] == p[j ],則next[j+1] =next[k] + 1,否則繼續遞歸前綴索引k = next[k],而後重複此過程。

注意到沒,k字符在這裏起到的是最大長度值的作用,例如下面這張圖,

  • 當j = 2時,k =0, p[k]= A 則因爲p[3]!=p[k],所以 k =next[k]一直遞歸到 k =-1,則next[3] =k+1 =0;
  • 依次類推我們可以瞭解到當 p[j] =D 時,next[j] =k +1 =2 ;
  • 當p[j+1] = E時,next[j+1] = next[next[k]] +1 = 0;
    image

總結

整個分析到這裏基本上也就結束了,整個KMP算法的關鍵核心是求next函數,next函數需要我們聯繫到模式串的前後綴最大長度值。KMP算法雖然在字符串的匹配過程中著以高效,但依然存在着自己的一些弊端,一些大佬在此基礎之上又進一步地對算法進行了優化。

KMP算法代碼實現:

void getnext(Str substr,int next[])
{
    int i = 1,j = 0;
    next[1] = 0;
    while(i < substr.length)
    {
        if(j==0||substr.ch[i] == str.ch[j])
        {
            i++;
            j++;
            next[i] = j;
        }
        else
        {
            j =next[j]
        }
    }
}

int KMP(Str str, Str substr, int next[])
{
    int i=1,j=1;
    while(i<= str.length && j <= substr.length)
    {
        if(j == 0 || str.ch[i] == substr.ch[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j =next[j];
        }
        
    }
    if(j > substr.length)
    {
        return i -substr.length;
    }
    else
    {
        return 0;
    }
}

關於優化的過程主要是next函數向nextval函數的轉變,關於優化方法,我會在以後單寫一篇文章用來講述,敬請關注!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章