1. 程式人生 > >【詳解】KMP算法

【詳解】KMP算法

是不是 代碼 ++ 大牛 bilibili 開始 最長 [] 分別是

前言

KMP算法是學習數據結構 中的一大難點,不是說它有多難,而是它這個東西真的很難理解(反正我是這麽感覺的,這兩天我一直在研究KMP算法,總算感覺比較理解了這套算法,

在這裏我將自己的思路分享給大家,也是檢驗一下自己有沒有真正掌握這個算法,錯誤的地方也請大家指正。嚶嚶嚶~~~

註:可供參考的資料有很多,視頻的話個人推薦B站的UP主正月點燈籠,博客的話有很多,不過不要貪多,不然容易混亂,不同的人對這個算法也是有不同的理解的!

背景

了解一個算法你要明白它的出處,KMP算法是由三位大牛同時研究出來的,他們分別是D.E.Knuth、J.H.Morris和V.R.Pratt,好吧其實明白他的出處也沒太大用。。。(逃

那麽KMP算法主要用來解決哪一方面的問題呢? 主要是用來解決字符串中的模式串(通俗點說是關鍵字)在主串中的定位問題,比較通俗點說就是找你這個模式串在你這個主串的什麽位置,並把它表示出來。

暴力匹配思路

看到這你就會感覺,這不是很簡單嗎,然後我們就會萌生下面這種思路:

有兩個字符串:一個是“BDABCDBCDABCDAC”,另一個是“BCDAC”

直接從第一個字符開始比較,發現第一個字符相同,再往後找,發現第二個不相同,就把BCDAC往後移一位,從主串的第二個開始往後匹配,這不就完了??簡單粗暴!

但是其實這樣做浪費了很多的時間,簡單來說:

如果你的兩個字符串是這樣的:一個是“AAAAAAAAAAAAAAAAB”,另一個是“AB”

那麽你可以很清楚的發現一個問題,這種暴力匹配算法,真的太剛了,簡直就是鐵頭娃,那麽我們應該怎麽解決這個問題呢?這就要講到我們的重點:KMP算法

NEXT數組

求next數組是KMP算法裏面最為重要的一部分,求出這部分也就幾乎成功了一半,那麽我們求的這個數組到底是用來做什麽的呢?其實是為了找到模式串失配後的下一個匹配位置

從而省去一些不必要的操作。求解next數組就不得不說到最長前後綴問題:

next數組的各元素是用來存放模式串的最長前後綴的長度,比如ABCDABD這個模式串,我們可以把它分成:

"A”、“AB”、“ABC”、“ABCD”、“ABCDA”、“ABCDAB”、“ABCDABD”七部分,分別求得他們的最長前後綴(前後綴不包含自身)是:

“A”:0 ,“AB”:0 ,“ABC”:0 ,“ABCD”:0 ,”ABCDA“:1,”ABCDAB“:2,”ABCDABD“:0

所以我們得到的next數組為{0,0,0,0,1,2,0} 怎麽樣?很簡單易懂吧,那麽我們應該怎麽用代碼來實現呢?

一般來說我們都會把next數組的第一位設為0,因為第一個字符的最大前後綴始終為0,至於有的設為-1,雖然之前我都是按照-1做的,但那只是版本不同,我們這種的思路是比那種要清晰的。

代碼實現如下:

void get_next(const char P[],int next[])
{
    int i,len;//i:模式串下標;len:最大前後綴的長度
    int m = strlen(P);//模式串長度
    next[0] = 0;//模式串的第一個字符的最大前後綴長度為0
    for (i = 1,len = 0; i < m; ++i)//從第二個字符開始,依次計算每一個字符對應的next值
    {
        while(len > 0 && P[i] != P[len])//遞歸的求出P[0]···P[i]的最大的相同的前後綴長度len
            len = next[len-1];         
        if (P[i] == P[len])//如果相等,那麽最大相同前後綴長度加1
        {
            len++;
        }
        next[i] = len;
  } }

看完上面這段代碼以後,我們發現最難懂的地方就是上面的while循環了,至於為什麽要這樣寫呢? 你可以理解為:如果模式串ABCDABD中進行到A,我們要填next[1]時,

發現A後的這個B和前面的A不相同,那麽我們的len是不會變的,所以我們要確保它等於上一個字符的next值。

KMP算法

有了next數組,我們就可以很好地實現KMP算法了,下面給出代碼:

void kmp(const char T[],const char P[],int next[])
{
    int n,m;
    int i,q;
    n = strlen(T);
    m = strlen(P);
    get_next(P,next);
    for (i = 0,q = 0; i < n; ++i)
    {
        while(q > 0 && P[q] != T[i])//如果模式串和主串不匹配,看不懂下面會講
            q = next[q-1];
        if (P[q] == T[i])//如果二者匹配,q加一
        {
            q++;
        }
        if (q == m)//如果全部匹配成功了,輸出位置
        {
            printf("%d\n",i-m+2);
        }
    }    
}

那麽為什麽要寫while那一句呢?其實原因很簡單,我們的next數組是表示的每一段的最長前後綴的長度,如果失配了,我們就會返回與模式串失配位置前相同的後面那一部分,

比如說主串為”ABCABDCABCDABD“,模式串為”ABCDABD“,

當我們進行到ABC後我們發現q=4時失配了,這時我們應該返回的應該是next[q-1],即它前一位的next數組,即next[3],即標紅色的那一部分,從那再開始進行,

也就是應該進行 "ABCDABD",這樣以此類推,仔細想想是不是這樣,這一段和next數組都是比較難理解的,但也是最關鍵的。

總結

相信各位巨巨在看完以上講解以後也基本理解了KMP算法,把它從頭到尾想一遍,發現其實它也不是很難,無非就是找一個next數組和進行一次KMP查找而已,接下來我們分析一下KMP算法的時間復雜度:

假設現在主串T匹配到 i 位置,模式串P匹配到 q 位置

  1.  如果q>0並且P[q] ! = T[i],即匹配失敗那麽q=next[q-1],模式串也就相當於主串向右移動了q-next [q-1] 位。
  2.  如果P[q]==T[i],表示匹配成功,那麽q++,往後移。

我們發現如果某個字符匹配成功,模式串q++;如果匹配失配,i 不變(即 i 不回溯),模式串會跳過匹配過的next [q-1]個字符。

當然我們做最壞的打算,當模式串首字符位於i-(q-1)的位置時才匹配成功,算法結束。
所以,如果主串的長度為n,模式串的長度為m,那麽匹配過程的時間復雜度為O(n),加上計算next的O(m)時間,KMP的整體時間復雜度為O(m + n)。

  

代碼

#include<stdio.h>
#include<string.h>
void get_next(const char P[],int next[])
{
    int i,len;//i:模式串下標;len:最大前後綴的長度
    int m = strlen(P);//模式串長度
    next[0] = 0;//模式串的第一個字符的最大前後綴長度為0
    for (i = 1,len = 0; i < m; ++i)//從第二個字符開始,依次計算每一個字符對應的next值
    {
        while(len > 0 && P[i] != P[len])//遞歸的求出P[0]···P[i]的最大的相同的前後綴長度len
            len = next[len-1];
        if (P[i] == P[len])//如果相等,那麽最大相同前後綴長度加1
        {
            len++;
        }
        next[i]=len;
    }
}

void kmp(const char T[],const char P[],int next[])
{
    int n,m;
    int i,q;
    n = strlen(T);
    m = strlen(P);
    get_next(P,next);
    for (i = 0,q = 0; i < n; ++i)
    {
        while(q > 0 && P[q] != T[i])//如果模式串和主串不匹配,看不懂下面會講
            q = next[q-1];
        if (P[q] == T[i])//如果二者匹配,q加一
        {
            q++;
        }
        if (q == m)//如果全部匹配成功了,輸出位置
        {
            printf("%d\n",i-m+1);
        }
    }
}

int main()
{
    int i;
    int next[20]={0};
    char T[] = "ABCABDCABCDABD";
    char P[] = "ABCDABD";
    printf("主串:%s\n",T);
    printf("模式串:%s\n\n",P );
    // get_next(P,next);
    printf("位置:");
    kmp(T,P,next);
    printf("\n");
    printf("next數組:\n");
    for (i = 0; i < strlen(P); ++i)
    {
        printf("%d ",next[i]);
    }
    printf("\n");
    return 0;
}

後記

花了兩個多小時,終於是打完了,KMP的講解就到這裏了,關於KMP的各項優化這裏也就不再多說,感興趣的話可以去baidu搜索BM算法和Sunday算法,

如果發現上文有什麽錯誤之處,還請隨時指正,謝謝!

                                                        ------------BY 孑、然---------------

                                                        --------2018.8.18 11:01-----------

                                                        --------------------------------------

【詳解】KMP算法