KMP算法詳解

前言

      前幾天,忽然聽到一位剛剛面試完應聘者的同事吐槽到「如今的程序員基本功怎麼這麼差,連一個簡單的KMP算法都搞不定,還好意思開那麼高的薪水"。聽到這裏,筆者默默的翻出《數據結構》,打開google。本文正是在這樣的背景下對KMP算法的複習與整理。html

簡介

       該算法是一種改進的字符串匹配算法,由D.E.Knuth與V.R.Pratt和J.H.Morris同時發現,所以稱之爲KMP算法。此算法能夠在O(n+m)的時間數量級上完成串的模式匹配操做。程序員

思想

       舉例來講,有一個字符串"BBC ABCDAB ABCDABCDABDE",我想知道,裏面是否包含另外一個字符串"ABCDABD"?面試

       

      首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一個字符與搜索字符串"ABCDABD"的第一個字符,進行比較。由於B與A不匹配,因此搜索詞後移一位。算法

    

      由於B與A不匹配,搜索字符串再日後移。數組

      

      就這樣,直到字符串有一個字符,搜索字符串的第一個字符相同爲止。網絡

      

     接着比較字符串和搜索字符串的下一個字符,仍是相同。數據結構

     

      直到字符串有一個字符,與搜索字符串對應的字符不相同爲止。函數

      

     這時,最天然地方式就是將搜索字符串整個後移一位,再從頭逐個比較。這樣作雖然可行,可是效率不好,由於你要把"搜索位置"移到已經比較過的位置,重比一遍。其算法時間複雜度即爲O(m*n)。優化

     

      一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"。KMP算法的關鍵思想就是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向後移,這樣就提升了效率。google

     

      怎麼作到這一點呢?能夠針對搜索字符串,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,後面再介紹,這裏只要會用就能夠了。

      

      已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最後一個匹配字符B對應的"部分匹配值"爲2,所以按照下面的公式算出向後移動的位數:

  右移位數 = 已匹配的字符數 - 對應的部分匹配值

     6-2=4, 則將搜索字符串後移4位。

     

     由於空格與C不匹配,搜索字符串還要繼續日後移。這時,已匹配的字符數爲2("AB"),對應的"部分匹配值"爲0。因此,移動位數 = 2 - 0,結果爲 2,因而將搜索字符串向後移2位。

      

      由於空格與A不匹配,繼續後移一位。

      

      逐位比較,直到發現C與D不匹配。因而,移動位數 = 6 - 2,繼續將搜索字符串向後移動4位

       

     逐位比較,直到搜索字符串的最後一位,發現徹底匹配,因而搜索完成。若是還要繼續搜索(即找出所有匹配),移動位數 = 7 - 0,再將搜索字符串向後移動7位,這裏就再也不重複了。

部分匹配表的生成

     從上面的匹配過程,咱們發現部分匹配表是KMP算法的關鍵所在,解下來讓咱們看一下部分匹配表是如何生成的。

     首先,咱們須要瞭解兩個概念:"前綴"和"後綴"。 "前綴"指除了最後一個字符之外,一個字符串的所有頭部組合;"後綴"指除了第一個字符之外,一個字符串的所有尾部組合。

     字符串「string」爲例,則「string」的前綴即爲: 「s", "st", "str", "stri", "strin"。其後綴即爲: "g", "ng", "ing", "ring", "tring"。

     "部分匹配值"就是"前綴"和"後綴"的最長的共有元素的長度。以"ABCDABD"爲例,

     

字符串 前綴 後綴 部分匹配值
A 空集 空集 0
AB A B 0
ABC A, AB C, BC 0
ABCD A, AB, ABC D, CD, BCD 0
ABCDA A, AB, ABC, ABCD A, DA, CDA, BCDA, 1
ABCDAB A, AB, ABC, ABCD, ABCDA B, AB, DAB, CDAB, BCDAB 2
ABCDABD A, AB, ABC, ABCD, ABCDA, ABCDAB D, BD, ABD, DABD, CDABD, BCDABD 0

     "部分匹配"的實質是,有時候,字符串頭部和尾部會有重複。好比,"ABCDAB"之中有兩個"AB",那麼它的"部分匹配值"就是2("AB"的長度)。搜索字符串移動的時候,第一個"AB"向後移動4位(字符串長度-部分匹配值),就能夠來到第二個"AB"的位置。

     

實現

      在KMP算法中有個數組,叫作前綴數組,也有的叫next數組,每個子串有一個固定的next數組,它記錄着字符串匹配過程當中失配狀況下能夠向前多跳幾個字符,固然它描述的也是子串的對稱程度,程度越高,值越大,固然以前可能出現再匹配的機會就更大。next數組的求法是KMP算法的關鍵,可是理解next數組並非一件輕鬆的事情。

      由上文,咱們已經知道,字符串「ABCDABD」各個前綴後綴的最大公共元素長度分別爲:

 

      並且,根據這個表能夠得出下述結論

  • 失配時,模式串向右移動的位數爲:已匹配字符數 - 失配字符的上一位字符所對應的最大長度值
      上文利用這個表和結論進行匹配時,咱們發現,當匹配到一個字符失配時,其實不必考慮當前失配的字符,更況且咱們每次失配時,都是看的失配字符的上一位字符對應的最大長度值。如此,便引出了next 數組。
      給定字符串「ABCDABD」,可求得它的next 數組以下:

      把next 數組跟以前求得的最大長度表對比後,不難發現,next 數組至關於「最大長度值」 總體向右移動一位,而後初始值賦爲-1。意識到了這一點,你會驚呼原來next 數組的求解居然如此簡單!

      換言之,對於給定的模式串:ABCDABD,它的最大長度表及next 數組分別以下:

    根據最大長度表求出了next 數組後,從而有

右移位數 = 失配字符所在位置 - 失配字符對應的next 值

    然後,你會發現,不管是基於《最大長度表》的匹配,仍是基於next 數組的匹配,二者得出來的向右移動的位數是同樣的。

    接下來,我們來寫代碼求下next 數組。

    基於以前的理解,可知計算next函數的方法能夠採用遞推,若是對於值k,有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,至關於next[j-1] = k。那麼對於pattern的前j 個序列字符,得

  • 若pattern[k] == pattern[j],則next[j] = next(j-1) + 1 = k + 1
  • 若pattern[k ] ≠ pattern[j],至關於在字符p[k]以前不存在前綴"p0 p1, …, pk-1"跟後綴「pj-k pj-k+1, …, pj-1"相等,那麼是否可能存在另外一個值t<k,使得p0 p1, …, pk-1 = pj-t pj-t+1…pj-1成立呢?這個t 顯然應該是next[k],由於這至關於一個"利用next函數值進行T串和T串的匹配"問題。

    求next數組以下:

    

 1 void getNext(const char *pattern, int *next, int pattern_len)
 2 {
 3     int i = 0;
 4     int j = -1;
 5     next[0] = -1;
 6 
 7     while (i < pattern_len - 1)
 8     {
 9 
10         if (j == -1 || pattern[i] == pattern[j])
11         {
12             ++i;
13             ++j;
14             if (pattern[i] != pattern[j]) //正常狀況
15                 next[i] = j;
16             else //特殊狀況,這裏即爲優化之處。考慮下AAAAB, 防止4個A造成012在匹配時屢次迭代。
17                 next[i] = next[j];
18         }
19         else
20         {
21             j = next[j];
22         }
23 }

     完整代碼以下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 
 5 
 6 static inline void getNext(const char *pattern, int *next, int pattern_len)
 7 {
 8     int i = 0;
 9     int j = -1;
10     next[0] = -1;
11 
12     while (i < pattern_len - 1)
13     {
14 
15         if (j == -1 || pattern[i] == pattern[j])
16         {
17             ++i;
18             ++j;
19             if (pattern[i] != pattern[j]) //正常狀況
20                 next[i] = j;
21             else //特殊狀況,這裏即爲優化之處。考慮下aaaab, 防止4個a造成012在匹配時屢次迭代。
22                 next[i] = next[j];
23         }
24         else
25         {
26             j = next[j];
27         }
28     }
29 }
30 
31 static inline bool match(const char *src, const char *pattern)
32 {
33     bool is_match = true;
34 
35     int src_index = 0;
36     int pattern_index = 0;
37     int src_len = strlen(src);
38     int pattern_len = strlen(pattern);
39 
40     //建立next數組,並初始化
41     int *next = (int *)malloc(pattern_len * sizeof(int));
42     getNext(pattern, next, pattern_len);
43 
44     //匹配主循環體
45     while (pattern_index < pattern_len && src_index < src_len)
46     {
47         //若對應位置字符匹配則右移1位,不然移動pattern
48         if (pattern_index == -1 || src[src_index] == pattern[pattern_index])
49         {
50             src_index++;
51             pattern_index++;
52         }
53         else
54         {
55             pattern_index = next[pattern_index];
56         }
57     }
58 
59     //若pattern_index未達到串尾,代表pattern未完成匹配。不然便是完成匹配
60     if (pattern_index >= pattern_len)
61     {
62         is_match = true;
63     }
64     else
65     {
66         is_match = false;
67     }
68 
69     return is_match;
70 }
71 
72 
73 int main(void)
74 {
75     char src[] = "aaaabacdeg";
76     char pattern[] = "aabacd";
77 
78     bool res = match(src, pattern);
79     printf("res: %d\n", (int)res);
80 
81     return 0;
82 }

 

備註

      本文有至關分量的內容參考借鑑了網絡上各位網友的熱心分享,特別是一些帶有徹底參考的文章,其後附帶的連接內容更直接、更豐富,筆者只是作了一下概括&轉述,在此一併表示感謝。

參考

      《字符串匹配的KMP算法

      《從頭至尾完全理解KMP

      《KMP算法的前綴next數組最通俗的解釋

相關文章
相關標籤/搜索