KMP算法理解

作者:高城
鏈接:https://www.zhihu.com/question/36149122/answer/66867065
來源:知乎

著作權歸作者所有,轉載請聯繫作者獲得授權。

面試官:

現在請你把我當做完全不瞭解KMP算法的人,向我解釋一下KMP算法的原理。

麪霸:

KMP算法俗稱“看(K)毛(M)片(P)算法”,用於快速文本匹配。試想你有兩條字符串src和pat,需要判斷pat是否在src中出現,如果出現就給出出現的具體位置。假設src的長度爲N,pat的長度爲M,首先我們來討論一下一個平凡的蠻力解。

如果你想判斷兩個字符串是否相等,你會寫一個這樣的循環(在紙上寫下):

for(int i = 0; i < M; i++) if(src[ i ] != dst[ i ]) return false;
return true;

這裏你最多比較了M次。回到原來的問題,有了這個子函數,你只要依次判斷(一邊在紙上寫符號)src[i, i + M - 1], i = 0, 1, 2, … 與dst是否相等就行了。複雜度是N乘以M。

那麼如何優化呢?我換一種說法,爲什麼能夠優化呢?因爲你做了重複的事情。我們換一種寫法,就能更直觀地發現重複在哪裏。

void bruteMatch(char* src, char* pat, vector<int> &ans)
{
    int N = strlen(src), M = strlen(pat);
        if(M == 0 || N < M) return;
    int i = 0, j = 0;
    while(i <= N - M)
    {
                j = 0;
        while(j < M && src[i] == pat[j])
        {
            i++; j++;
        }
        if( j == M )    ans.push_back(i - M);
        i = i - j + 1;
    }
}

你看這個變量i(一邊用手指),它每次比較完一輪,就退回到原來的位置的下一個位置重新開始匹配。難道我剛剛做了那麼多次比較,得到的信息就白白扔掉了嗎?KMP三人找到了一種簡單的利用這信息的方法,只要花點力氣對模式串pat做一下預處理,就能使這匹配程序裏的循環變量i只進不退,從而達到N+M的複雜度。

我們把這個匹配過程想象成,模式串依附着源串向後移動。你看(一邊畫圖),i在這個位置,j在這個位置,走了這麼一段路才發生失配了,意味着這兩條(src[i - j, i - 1]和pat[0, j-1])是公共子串,也就是說此時src的這條子串的信息完全包含於pat的前綴子串之中,原則上我如果對pat做了充分的瞭解,就可以保持i不變,而單單令pat往後移動。假如我可以令pat移動一步而i不變,那說明這兩大段是相等的;假如這兩大段不相等,那麼我至少可以令pat移動兩步。觀察發現,我應該令pat移動多少步,取決於pat[0, j - 1]的最長的相等{前綴、後綴}的長度。

KMP的精髓就在於,用了一個線性的算法,得到了每次在pat[ j ]發生失配時,應該讓pat往後移動多少步,這個值對應於pat[0, j - 1]的最長相等{前綴、後綴}的長度。這些值所存的數組叫做next數組。

我需要寫出計算next數組的函數,才能具體解釋這個算法。

面試官:

可以了,說到最長相等前後綴這點就足夠了。接下來請你手寫一個歸併排序的代碼……

附一套高效的KMP代碼:

void get_next(const string &pat, vector<int> &next)
{
        // 通過該函數得到next數組之後,當在src[i]和pat[j]處發生失配時,保持i不動,
        // j變更爲next[j],就相當於把pat向後移動了(j - next[j])步
    int i = 0, j = -1, p_len = pat.length();
    next[0] = -1;
    while (i < p_len)
    {
        if (j == -1 || pat[i] == pat[j])
        {
            i++;
            j++;
            if (pat[i] != pat[j])
                next[i] = j;
            else
                next[i] = next[j];
        }
        else
        {
            j = next[j];
        }
    }
}

這個函數的過程可以看作將pat和自身做匹配的過程。第13行至第16行的判斷比較令人費解,其實那是一個加速優化。語句j = next[ j ]也可以看作用動態規劃做的優化。get_next可以寫成下面的非優化形式,並且它的複雜度也是O(M):

void get_next(const string &pat, vector<int> &next)
{
        // 通過該函數得到next數組之後,當在src[i]和pat[j]處發生失配時,保持i不動,
        // j變更爲next[j],就相當於把pat向後移動了(j - next[j])步
    int i = 0, j = -1, p_len = pat.length();
    next[0] = -1;
    while (i < p_len)
    {
        if (j == -1 || pat[i] == pat[j])
        {
            i++;
            j++;
            next[i] = j;
        }
        else
        {
            j--;
        }
    }
}

然後是利用next數組進行匹配的代碼:

//add the positions to a vector
void indice_kmp(const string &str, const string &pat, vector<int> &ans)
{
    int i = -1, j = -1;
    int len1 = str.length(), len2 = pat.length();
    vector<int> next(len2 + 2, 0);
    get_next(pat, next);
    while(i < len1)
    {
        if (j == -1 || str[i] == pat[j])
        {
            i++;
            j++;
            if(j == len2){
                ans.push_back(i - len2);
            }
        }
        else
        {
                        // (j - next[j])就是pat向後移動的步數
            j = next[j];
        }
    }
}

關於複雜度:無論是get_next還是indice_kmp,在每個while循環單體中,存在一個嚴格增量:i + (模式串往後移動的步數),因此該算法的複雜度是O(N + M).

最後說一句:看毛片有害身心健康。

有首歌是這麼唱的:
You VOTE ME UP~~~~ so I can stand on moutains ~~~~

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