KMP算法詳解


前言

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

簡介

       該算法是一種改進的字符串匹配算法,由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算法的關鍵思想就是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向后移,這樣就提高了效率。

     

      怎么做到這一點呢?可以針對搜索字符串,算出一張《部分匹配表》(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數組最通俗的解釋


免責聲明!

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



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