1. 程式人生 > >ACM-字串-模式串匹配-KMP演算法

ACM-字串-模式串匹配-KMP演算法

在模式匹配演算法中,KMP是比較常見的單模、高效率演算法之一。在討論KMP之前,先看看樸素的匹配演算法為什麼低效。普通的暴力匹配演算法在每一次匹配失敗之後,僅僅下移一位,並且需要重新判斷整個模式串的每一個字元,見下圖:


第一次匹配時,首先會遍歷模式串的每一個字元,但是發現模式串的第4個字元f,與文字串的第4個字元a不匹配,所以此時匹配失敗;接著進行第二次匹配,文字串下移一位,即從第1個字元開始,然後同樣會遍歷模式串的每一個字元。這樣樸素的匹配過程,假設文字串T的長度為n,模式串P的長度為m,那麼可能的匹配起始點有n-m個,而每一次匹配都需要遍歷整個模式串P,所以時間複雜度是冪次級別的o(m*(n-m))。顯然這樣複雜度的做法是比較低效的。

接下來看下KMP演算法是怎麼優化匹配過程的。還是分析前面的例子,當第一次匹配失敗的時候,樸素演算法的做法是將文字串下移一個字元,然後對於模式串則從頭開始逐個字元的判斷,即將文字串的第1個字元b與模式串的第0個字元a進行比較。然而其實將這一對字元進行比較是沒有必要的,因為仔細觀察第一次匹配過程可以知道,當第一次匹配失敗的時候,文字串的0-3號字元已經確認是abba了,那麼也就是說此時已經確定了模式串的第0個字元a與文字串的第1個字元b肯定是不相等的。同理,將模式串的第0個字元與文字串的第2個字元進行比較也是沒有必要的。接下來,看模式串的第0個字元與文字串的第3個字元a,當然是相等的,這種情況下,就將文字串和模式串都下移一位,用同樣的方式進行比較(此例的比較已完成,不用下移繼續比較),以此類推,直到判斷到文字串的第3個字元,也就是說將上一次匹配失敗時所已經確定的文字串字元都判斷完畢為止。改進後匹配過程如下圖:


這正是KMP演算法的匹配過程,即當文字串在第4個字元失配的時候,不是按照樸素匹配演算法將文字串下移一位將匹配起始位變成第1個元素b,然後再從頭對模式串的每一個字元進行判斷的做法,而是利用已經得到確認的文字串字元資訊,直接將失配的文字串第4個字元,與模式串的第1個字元進行比較,省去中間一些沒有必要進行的比較。由此可見,kmp演算法將模式匹配過程的時間複雜度優化到了o(m+n)。

由上述分析可知,KMP演算法進行匹配的思路就是:利用上一次失配時已經確認了的文字串字元資訊,來決定下一次匹配時模式串應該用來進行比對的字元位置,而文字串用來比對的字元位置則是失配時的那個字元位置。

在上面的例子中,當文字串在第4個字元失配時,按照KMP演算法的思路,應該直接將失配字元與模式串的第1個字元進行比較。經過前面的分析,已經確認了這是正確的,所以下一步的關鍵則是失配時模式串字元的位置應該如何計算呢?當然,不能直接將上面分析的過程翻譯成演算法,因為那只是一個人為的判斷過程,如果要寫出固定的邏輯,必須要找到相關的規律。仔細觀察匹配的過程不難發現,每一次匹配所確定的文字串字元一定是模式串的某一個字首,因為當第一次匹配開始的時候一定是從模式串的第一個字元開始判斷的。再仔細觀察文字串失配後,KMP演算法進行的最後結果,其實就是將模式串的某一個字首與已確定文字串字元的某一個字尾進行重合了。如下圖所示:


其中,灰色部分是已確認文字串字元,文字串在第i個字元a處失配,而B是已確定文字串字元的一個字尾,A則是模式串的一個字尾,如果此時是下一輪比較的正確起始位置,那麼狠明顯的一個條件就是A和B必須要相等,並且按照KMP跳過所有不需要比較字元的思路,還要求A和B的長度必須儘可能的大。現在問題就變成了:尋找已確定文字串字元的一個最大字尾與模式串的一個最大字首相等。再次思考KMP演算法的思路:利用上一次失配時已經確認了的文字串字元資訊。因為是模式匹配,要確認文字串的字元具體是什麼,一定是用模式串來判斷的,而模式串肯定是已經確定了的。由上圖也可以看出,已確定的文字串字元,其實就是已經判斷過了的模式串字元,而它們都是模式串的一個字首。那麼問題就進一步轉化為:尋找當前模式串字首的最大公共前後綴。由於失配可能發生在模式串的任意一個字元,所以失配時的當前模式串字首可能是模式串的任意一個字首。所以當模式串在第i個字元失配的時候,假設當前模式串字首的最大公共前後綴的長度為l,那麼下一輪匹配模式串移動k位後將要進行比較的是第l個字元。這個l值,其實也就是KMP演算法中著名的next陣列值,即next[i]=l。

現在已經知道了要計算的是模式串的每一個字首的最大公共前後綴的長度。這一步可以利用字串前後綴的遞推屬性,用模式串本身來匹配本身進行計算,原理如下圖所示:


現在要計算next[i+1]的值。已知next[i]代表的是模式串字首T[0-i]的最大公共前後綴長度,那麼這個字首的下一個字元位置即等於next[i],而這個字尾的下一個字元位置就是當前的i+1,如上圖中的第二個串所示。那麼此時比較next[i]位置和i+i位置上的字元,這其實就是一個KMP的模式匹配的過程了,如果相等則可知當前位置的最大公共前後綴是前一個位置i的最大公共前後綴加1;如果不相等,則只能在next[i]位置之前繼續尋找與i+1位置相等的字元,並且利用已經計算出來的最大公共前後綴資訊跳過不不必要的比較,如下圖:


由此,可以寫出計算next陣列的演算法:

// 模式串最大長度
const int MAX_P_LEN = 1024;
// next陣列,next[i]代表模式串字首pattern[0-i]的最大公共前後綴
// 同時,next[i]也代表當模式串在第i個位置字元失配時,下一次應該用來與當前位置的文字串字元繼續比較的模式串字元位置
int next[MAX_P_LEN];

// 構建模式串pattern的next陣列值
void getFail(char pattern[])
{
    int PatLen = strlen(pattern);
    // 初始化遞推邊界
    next[0] = 0;
    next[1] = 0;
    // 構建模式串pattern的每一個字首的next值,即計算其最大公共前後綴的長度
    for(int i=1; i<PatLen; ++i)
    {
        int j = next[i];
        // 在字首中遞推搜尋
        while(j && pattern[i]!=pattern[j]) j = next[j];
        next[i+1] = pattern[i]==pattern[j] ? j+1 : 0;
    }
}

當next陣列的值全部被計算出來之後,模式匹配的過程就比較簡單了:分別從文字串和模式串的第一個字元開始判斷,如果相等則比較下一個字元,否則失配時則按照對應位置的next值移動模式串重新進行比較,直到判斷完所有的模式串字元則匹配成功,判斷完所有的文字串字元則整個匹配過程結束。具體演算法程式碼如下:
// 用於判斷文字串text中是否包含模式串pattern
int kmp(char text[], char pattern[])
{
    int TextLen = strlen(text);
    int PatLen = strlen(pattern);
    // 當前比較的模式串字元位置
    int j = 0;
    // 比較文字串的每一個字元
    for(int i=0; i<TextLen; ++i)
    {
        // 失配時沿著失配邊走,直到可以匹配或回到模式串的第一個字元
        while(j && text[i]!=pattern[j]) j = next[j];
        // 比較當前文字串字元和移動後的模式串當前字元
        if (text[i] == pattern[j]) ++j;
        // 如果模式串的所有字元都比較相等了,則完成模式匹配
        if (j == PatLen) return 1;
    }
    return 0;
}

上面的kmp程式碼在第一次匹配成功後就返回了,也就是說僅僅只能判斷文字串是否包含模式串。其實kmp還能找出模式串在文字串中所有出現的位置,即模式串出現的次數。具體做法也很簡單,就是當每一次模式串匹配成功後,利用最後一個next陣列值來移動模式串,繼續進行模式匹配。匹配演算法程式碼如下:
// 用於判斷模式串pattern在文字串text中出現了多少次
int kmp_times(char text[], char pattern[])
{
    int times = 0;
    int TextLen = strlen(text);
    int PatLen = strlen(pattern);
    // 當前比較的模式串字元位置
    int j = 0;
    // 比較文字串的每一個字元
    for(int i=0; i<TextLen; ++i)
    {
        // 失配時沿著失配邊走,直到可以匹配或回到模式串的第一個字元
        while(j && text[i]!=pattern[j]) j = next[j];
        // 比較當前文字串字元和移動後的模式串當前字元
        if (text[i] == pattern[j]) ++j;
        // 如果模式串的所有字元都比較相等了,則完成模式匹配
        if (j == PatLen)
        {
            ++times;
            // 移動模式串
            j = next[j];
        }
    }
    return times;
}