KMP字符串匹配算法

1. KMP算法基本思想

問題:在字符串ABABABACA中尋找字符串ABABACA,並返回第一次出現的位置。
下面分析匹配過程

ABABABACA
ABABACA
     |此處出現不匹配

若此時按照樸素字符串匹配算法進行匹配,模式字符串在不匹配的時候右移一位,重新從第一個字符進行匹配,情況如下

ABABABACA
 ABABACA
 |右移一位,重新從第一個字符進行匹配,很明顯不匹配,無效偏移
ABABABACA
  ABABACA
  |再次右移一位,重新從第一個字符進行匹配,一直到模式串末尾,匹配成功

能否避免無效偏移和每次都從頭開始匹配?這就是KMP算法所實現的。

ABABABACA
ABABACA
     |此處出現不匹配,將該位置記爲pos
ABABABACA
  ABABACA
     |直接偏移2位
     |發現在上次出現不匹配的位置pos之前的3個字符ABA是匹配的,那麼就不需要從模式串頭開始匹配,直接從pos處進行匹配   

問題:ABABA和ABA是什麼關係?怎麼知道可以直接偏移2位?
ABA爲字符串ABABA的前綴和後綴的最長的共有字符串。
ABABA的前綴字符串(不包括尾字符)有A AB ABA ABAB
ABABA的後綴字符串(不包括頭字符)有A BA ABA BABA
所以ABABA的前綴和後綴的最長的共有字符串爲ABA,長度爲3

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

上述例子中,已匹配=5,部分匹配=3,所以移動位數=2

倘若算出每個位置的部分匹配值,就可以直接得到應該移動的位數,從而避免無效移位,這個要求的部分匹配值被稱爲部分匹配表(Partial Match Table)。

2. 如何求部分匹配表(next數組)?

next數組的前兩個元素爲-1,0

  A B A B A C A
 -1 0

求next[pos]要根據next[pos - 1]的值。

1. 當pos - 1處的字符與next[pos - 1]即cnd處字符相同時
如下圖所示,淺藍色是子串P[0..pos - 2]的最長前綴後綴公共字符串,並且兩個深藍色處字符相同,那麼子串P[0..pos - 1]的最長前綴後綴公共字符串長度爲next[pos - 1] + 1,即cnd + 1。
這裏寫圖片描述

2. 當pos - 1處的字符與next[pos - 1]即cnd處字符不相同時
如下圖所示,綠色方塊表示子串P[0..cnd - 1]的最長前綴後綴公共字符串,該綠色方塊字符串一定也會是子串P[0..pos - 2]的前綴後綴公共字符串(非最長),若next[cnd]處字符與pos - 1處字符相同,則next[pos] = next[cnd] + 1, 若不相同,重複上述步驟。
這裏寫圖片描述

實現代碼如下:

private int[] getNext(String p) {
    if (p.length() == 1)
        return new int[] {-1};
    int[] next = new int[p.length()];
    next[0] = -1;
    next[1] = 0;
    int pos = 2; // 當前計算位置爲2
    int cnd = 0; // 當前已經計算出的最長前綴後綴公共字符串的下一個字符位置
    while (pos < p.length()) {
        if (p.charAt(pos - 1) == p.charAt(cnd)) {
            next[pos++] = ++cnd;
        } else if (cnd > 0) {
            cnd = next[cnd];
        } else {
            next[pos++] = 0;
        }
    }
    return next;
}

3. 優化next數組

目前該算法實現並不完美。依然以模式串ABABACA爲例,然而此時的待檢測字符串爲ABABCABABACA。讓我們分析下匹配過程。

ABABCABABACA
ABABACA
    | 此處出現不匹配,根據部分匹配表,next[4] = 2,最長前綴後綴公共字符串爲AB,右移2位

ABABCABABACA
  ABABACA
    | 不匹配。注意,上一次是字符C與A進行比較,這一次依然是字符C與A比較,這一次也是一次無效偏移,這就是待優化的地方

優化方法爲判斷當前字符是否與前綴下一個字符相同,若相同,則next[pos] = next[cnd]。
優化結果

原next數組
 A B A B A C A
-1 0 0 1 2 3 0

改進後next數組
 A B A B A C A
-1 0 0 0 0 3 0

優化代碼如下

private int[] getNext(String p) {
    if (p.length() == 1)
        return new int[] {-1};
    int[] next = new int[p.length()];
    next[0] = -1;
    next[1] = p.charAt(0) == p.charAt(1) ? -1 : 0;
    next[1] = 0;
    int cnd = 0;
    int pos = 2;
    while (pos < p.length()) {
        if (p.charAt(pos - 1) == p.charAt(cnd)) {
            // 此處判斷當前字符是否與前綴下一個字符相同
            // 若相同,則next[pos] = next[cnd]
            if (p.charAt(pos) != p.charAt(++cnd))
                next[pos++] = cnd;
            else
                next[pos++] = next[cnd];
        } else if (next[cnd] > -1) {
            cnd = next[cnd];
        } else {
            next[pos++] = 0;
        }
    }
    return next;
}

4. 根據next數組實現線性字符串匹配

實現代碼如下

public int strStr(String str, String pattern) {
    if (str == null || pattern == null)
        return -1;
    if (pattern.length() == 0)
        return 0;
    int[] next = getNext(pattern);
    int m = 0; // 已匹配字符串頭在待檢測字符串str中的位置
    int i = 0; // 當前進行匹配的字符在模式串pattern中所處的位置
    while (m + i < str.length()) {
        if (str.charAt(m + i) == pattern.charAt(i)) {
            i++;
            if (i == pattern.length())
                return m;
        } else {
            if (next[i] == -1) {
                // 無前綴後綴公共字符串
                // 右移一位,從模式串頭開始匹配
                m++;
                i = 0;
            } else {
                // i爲已匹配長度 next[i]爲部分匹配長度 i - next[i]爲移動位數
                m = m + i - next[i]; // 右移
                i = next[i]; // 用部分匹配長度更新已匹配長度
            }
        }
    }
    return -1;
}

參考資料

Knuth–Morris–Pratt algorithm

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