KMP算法及優化

今天看到同窗在複習數據結構書上的KMP算法,突然發覺本身又把KMP算法忘掉了,之前就已經忘過一次,看樣子仍是沒有真正的掌握它,這回學聰明點,再次搞明白後記錄下來。c++


通常字符串匹配過程

KMP算法是字符串匹配算法的一種改進版,通常的字符串匹配算法是:從主串(目標字符串)模式串(待匹配字符串)的第一個字符開始比較,若是相等則繼續匹配下一個字符, 若是不相等則從主串的下一個字符開始匹配,直到模式串被匹配完,則匹配成功,或主串被匹配完且模式串未匹配完,則匹配失敗。匹配過程入下圖:算法

通常匹配方法.PNG

這種實現方式是最簡單的, 但也是低效的,由於第三次匹配結束後的第四次和第五次是沒有必要的數組

分析

第三次匹配在j = 0(a)i = 2(a)處開始,在j = 4(c)i = 6(b)處失敗,這意味着模式串和主串中:j = 0(a)i = 2(a)j = 1(b)i = 3(b)j = 2(c)i = 4(c)j = 3(a)i = 5(a)這四個字符相互匹配。 數據結構

分析模式串的前3個字符:模式串的第一個字符j = 0是aj = 1(b)j = 2(c)這兩個字符和j = 0(a)不一樣,所以以這兩個字符開頭的匹配一定失敗,在第三次匹配中,主串中i = 3(b)i = 4(c)和模式串j = 1(b)j = 2(c)相互匹配,所以匹配失敗後,能夠直接跳過主串中i = 3(b)i = 4(c)這兩個字符的匹配。 測試

繼續分析模式串的j = 3(a)j = 4(c)這兩個字符,若是模式串匹配到j = 4(c)這個字符才失敗的話,由於j = 4(c)的前一個字符j = 3(a)和第一個字符j = 0(a)是相同的,結合上一個分析得知:優化

1):下一次匹配中主串已經跳過了和j = 3(a)前兩個相互匹配的字符i = 3(b)i = 4(c),將從i = 5(a)開始匹配。
2):j = 3(a)i = 5(a)相互匹配。url

所以下一次匹配認爲j = 3(a)i = 5(a)已經匹配過了,而j = 3(a)和j = 0(a)是相同的,因此下次匹配能夠從j = 2(b)i = 6(b)開始,這樣的話也跳過了j = 0(a)和i = 5(a)這個字符的匹配。spa

同理可得第二次匹配也是不必的。code

KMP算法

KMP算法匹配過程

利用KMP算法匹配的過程以下圖:字符串

KMP算法匹配過程.PNG

KMP算法的改進之處在於:可以知道在匹配失敗後,有多少字符是不須要進行匹配能夠直接跳過的,匹配失敗後,下一次匹配從什麼地方開始可以有效的減小沒必要要的匹配過程。

next[n]求解方法

由上面的分析能夠發現,KMP算法的核心在於對模式串自己的分析,其分析結果能提供在j = n位置匹配失敗時,從j = 0j = n - 1這個子串中前綴和後綴的最長公共匹配的字符數,這樣說可能比較難以理解,看下圖:

最長公共匹配字符數.PNG

在獲得子串前綴和後綴的最長公共匹配字符數l後,之後在i = x,j = n處匹配失敗時,能夠直接從i = x,j = l處繼續匹配(證實過程參考:嚴蔚敏的《數據結構》4.3章),這樣問題就很明顯了,咱們要求出n和l對應的值,其中n是模式串字符數組的下標,l的有序集合一般稱之爲next數組,前面兩個模式串的next數組下標n的對應以下:

03134812_rdSI.png

模式串2完整匹配過程

有了這個next數組,那麼在匹配的過程當中咱們就能在j = n處匹配失敗後,根據next[n]的值進行偏移,其中next[0]固定爲-1,表明在當前i這個位置整個模式串和主串都沒法匹配成功,要從下一個位置i = i + 1j = 0處開始匹配,模式串2的匹配過程以下:

模式串2匹配過程.PNG

如今知道了next數組的做用,也知道在有next數組時的匹配過程,那麼剩下的問題就是如何經過代碼求出next數組匹配過程了。

next數組的過程能夠認爲是將模式串拆分紅n個子串,分別對每一個子串求前綴和後綴的最長公共匹配字符數l,這一點能夠經過上圖(最長公共匹配字符數)看出來(沒有畫出l=0時的圖解)看出來。

代碼實現

next數組的代碼以下:

void get_next(string pattern, int next[]) {
//    !!!!!!!!!!由網友(評論第一條)指出該算法存在問題,已將有問題的代碼註釋並附上臨時想到的算法代碼。

//    int i = 0; // i用來記錄當前計算的next數組元素的下標, 同時也做爲模式串自己被匹配到的位置的下標
//    int j = 0; // j == -1 表明從在i的位置模式串沒法匹配成功,從下一個位置開始匹配
//    next[0] = -1; // next[0]固定爲-1
//    int p_len = pattern.length();
//    while (++i < p_len) {
//        if (pattern[i] == pattern[j]) {
//            // j是用來記錄當前模式串匹配到的位置的下標, 這就意味着當j = l時,
//            // 則在pattern[j]這個字符前面已經有l - 1個成功匹配,
//            // 即子串前綴和後綴的最長公共匹配字符數有l - 1個。
//            next[i] = j++;
//        } else {
//            next[i] = j;
//            j = 0;
//            if (pattern[i] == pattern[j]) {
//                j++;
//            }
//        }
//    }
    
    int j = 0;
    next[0] = -1;
    int p_len = pattern.length();
    int matched = 0;
    while (++j <= p_len) {
        int right = j - 1;
        int mid = floor(right / 2);
        int left = right % 2 == 0 ? mid - 1 : mid;
        int curLeft = left;
        int curRight = right;
        while (curLeft >= 0) {
            if (pattern[curLeft] == pattern[curRight]) {
                matched++;
                curLeft--;
                curRight--;
            } else {
                matched = 0;
                curLeft = --left;
                curRight = right;
            }
        }
        next[j] = matched;
        matched = 0;
    }
}

根據next數組求模式串在主串中的位置代碼以下:

int search(string source, string pattern, int next[]) {
    int i = 0;
    int j = 0;
    int p_len = pattern.length();
    int s_len = source.length();
    while (j < p_len && i < s_len) {
        if (j == -1 || source[i] == pattern[j]) {
            i++;
            j++;
        }
        else {
            j = next[j];            
        }
    }
    if (j < pattern.length())
        return -1;
    else
        return i - pattern.length();
}

測試代碼以下:

int main() {
    string source = "ABCDABCEAAAABASABCDABCADABCDABCEAABCDABCEAAABASABCDABCAABLAKABCDABABCDABCEAAADSFDABCADABCDABCEAAABCDABCEAAABASABCDABCADABCDABCEAAABLAKABLAKK";
    // string pattern = "abcaaabcab";
    string pattern = "ABCDABCEAAABASABCDABCADABCDABCEAAABLAK";
    int next[pattern.length()] = { NULL };
    get_next(pattern, next);
    cout << "next數組: \t";
    for    (int i = 0; i < pattern.length(); i++)
        cout << next[i] << " ";
    cout << endl;
    int pos = search(source, pattern, next);
    if (-1 != pos) {
        cout << "匹配成功,模式串在主串中首次出現的位置是: 第" << pos + 1 << "位";
        getchar();
        return 0;
    } else {
        cout << "匹配失敗";
    }
    getchar();
    return 0;
}

執行結果:

next數組: -1 0 0 0 0 1 2 3 0 1 1 1 2 1 0 1 2 3 4 5 6 7 1 0 1 2 3 4 5 6 7 8 9 10 11 12 0 1 
匹配成功,模式串在主串中首次出現的位置是: 第97位

KMP算法優化

再回過頭去看模式串2的next數組的圖:

03134948_leUT.png

若是模式串和主串的匹配在j = 6(b)處失敗的話,根據j = next[6] = 1得知下一次匹配從j = 1處開始,j = 1處的字符和j = 6處的字符同爲c,所以此次匹配一定會失敗。
一樣的,模式串和主串的匹配在j = 7(c)處或在j = 9(b)處失敗的話,根據next數組偏移後下一次匹配也一定會失敗。

考慮若是模式串是: aaaac,根據通常的KMP算法求出的next數組及匹配過程以下:

KMP算法無效匹配過程.PNG

顯而易見,在第二次匹配失敗後,第3、4、五次匹配都是沒有意義的,j = next[3]、j = next[2]、j = next[1]、j = next[0]這四處的字符都是a,在j = 3(a)處匹配失敗時,根據模式串自己就應該能夠得出結論:能夠跳過j = 2(a)、j = 1(a)、j = 0(a)的匹配,直接從i = i + 1j = 0處開始匹配,因此優化事後的next數組應該是:

優化事後的next數組.PNG

代碼實現

優化後的求next數組的代碼以下:

void get_next(string pattern, int next[]) {
//    !!!!!!!!!!由網友(評論第一條)指出該算法存在問題,更新後的代碼在上方,新算法的優化代碼暫未實現,可是優化思路是正確的。

//    int i = 0; // i用來記錄當前計算的next數組元素的下標, 同時也做爲模式串自己被匹配到的位置的下標
//    int j = 0; // j == -1 表明從在i的位置模式串沒法匹配成功,從下一個位置開始匹配
//    next[0] = -1; // next[0]固定爲-1
//    int p_len = pattern.length();
//    while (++i < p_len) {
//        if (pattern[i] == pattern[j]) {
//            // j是用來記錄當前模式串匹配到的位置的下標, 這就意味着當j = l時,
//            // 則在pattern[j]這個字符前面已經有l - 1個成功匹配,
//            // 即子串前綴和後綴的最長公共匹配字符數有l - 1個。
//            next[i] = j++;
//
//            // 當根據next[i]偏移後的字符與偏移前的字符向同時
//            // 那麼此次的偏移是沒有意義的,由於匹配一定會失敗
//            // 因此能夠一直往前偏移,直到
//            // 1): 偏移前的字符和偏移後的字符不相同。
//            // 2): next[i] == -1
//            while (next[i] != -1 && pattern[i] == pattern[next[i]]) {
//                next[i] = next[next[i]];
//            }
//        } else {
//            next[i] = j;
//            j = 0;
//            if (pattern[i] == pattern[j]) {
//                j++;
//            }
//        }
//    }
}

結尾

但願本文能對你有幫助, 若是有什麼問題, 歡迎探討。

參考文獻

嚴蔚敏的《數據結構》4.3章
kmp算法--百度百科

相關文章
相關標籤/搜索