KMP算法的實現(Java)

KMP算法的實現(Java)

簡介

KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人們稱它爲克努特—莫里斯—普拉特操作(簡稱KMP算法)。KMP算法的核心是利用匹配失敗後的信息,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是通過一個next()函數實現,函數本身包含了模式串的局部匹配信息。KMP算法的時間複雜度O(m+n)

問題

有一個目標串S,和一個模式串P,現在要尋找模式串P是否在目標串S 中,及其出現的位置,怎麼查找呢?

暴力算法(Brute Force)

用暴力算法匹配字符串過程中,我們會把S[0] 跟 P[0] 匹配,如果相同則匹配下一個字符,直到出現不相同的情況,此時我們會丟棄前面的匹配信息,然後把S[1] 跟 P[0]匹配,循環進行,直到主串結束,或者出現匹配成功的情況。這種丟棄前面的匹配信息的方法,極大地降低了匹配效率。

假設現在目標串S匹配到 i 位置,模式串P匹配到 j 位置,則有:

  • 如果當前字符匹配成功(即S[i] == P[j]),則i++,j++,繼續匹配下一個字符;
  • 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置爲0。
/**
 * Brute Force: O(mn)
 * @param s 目標串
 * @param p 模式串
 * @return 如果匹配成功,返回下標,否則返回-1
 */
public static int stringMatch(String s, String p) {
    if (s.equals(p)) {
        return 0;
    }
    if (s.length() < p.length()) {
        return -1;
    }

    int i = 0, j = 0;
    while (i < s.length() && j < p.length()) {
        // 如果當前字符匹配成功(即S[i] == P[j]),則i++,j++,繼續匹配下一個字符
        if (s.charAt(i) == p.charAt(j)) {
            i++;
            j++;
        } else {
            // 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置爲0
            i = i - j + 1;
            j = 0;
        }
    }
    if (j == p.length()) {
        return i - j;
    } else {
        return -1;
    }
}

KMP算法

在KMP算法中,對於每一個模式串我們會事先計算出模式串的內部匹配信息,在匹配失敗時最大的移動模式串,以減少匹配次數。
在簡單的一次匹配失敗後,我們會想將模式串儘量的右移和主串進行匹配。右移的距離在KMP算法中是如此計算的:在已經匹配的模式串子串中,找出最長的相同的前綴和後綴,然後移動使它們重疊。

假設現在文本串S匹配到 i 位置,模式串P匹配到 j 位置:

  • 如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++,繼續匹配下一個字符;
  • 如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]。此舉意味着失配時,模式串P相對於文本串S向右移動了j - next [j] 位。
  • 換言之,當匹配失敗時,模式串向右移動的位數爲:失配字符所在位置j - 失配字符對應的next 值,即移動的實際位數爲:j - next[j],且此值大於等於1。

next 數組各值的含義:代表當前字符之前的字符串中,有多大長度的相同前綴後綴。例如如果next [j] = k,代表j 之前的字符串中有最大長度爲k 的相同前綴後綴。

在某個字符失配時,該字符對應的next 值會告訴你下一步匹配中,模式串應該跳到哪個位置(跳到next [j] 的位置)。如果next [j] 等於0或-1,則跳到模式串的開頭字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某個字符,而不是跳到開頭,且具體跳過了k 個字符。

在第一次匹配過程中:

i 0 1 2 3 4 5 6 7 8
s a b a c a b a b c
p a b a b
j 0 1 2 3

s[3] 與 p[3]出現不匹配,而s[0]~s[2]是匹配的,其中s[0] ~ s[2] 就是上文中說的已經匹配的模式串子串,移動找出最長的相同的前綴和後綴並使他們重疊:

i 0 1 2 3 4 5 6 7 8
s a b a c a b a b c
p a b a b
j 0 1 2 3

然後在從上次匹配失敗(i = 3)的地方進行匹配,這樣就減少了匹配次數,增加了效率。

next數組的計算

1. 找出最長的相同的前綴和後綴

以“abab”爲例:
在這裏插入圖片描述

2. next數組

next 數組相當於“最大長度值” 整體向右移動一位,然後初始值賦爲-1,即
在這裏插入圖片描述

/**
 * Table building: O(m)
 *
 * @param p 匹配串
 * @return
 */
private static int[] getNext(String p) {
    int len = p.length();
    int[] next = new int[len];
    next[0] = -1;
    int i = 0, k = -1;
    while (i < len - 1) {
        // p[k]表示前綴,p[i]表示後綴
        if (k == -1 || p.charAt(i) == p.charAt(k)) {
            ++k;
            ++i;
            next[i] = k;
        } else {
            k = next[k];
        }
    }
    return next;
}

3. 代碼實現

/**
* O(m+n)
*
* @param s 目標串
* @param p 模式串
* @return 如果匹配成功,返回下標,否則返回-1
*/
private static int kmpSearch(String s, String p) {
   int sLen = s.length();
   int pLen = p.length();
   if (sLen < pLen) {
       return -1;
   }

   int[] next = getNext(p);
   // matching: O(n)
   int i = 0, j = 0;
   while (i < sLen && j < pLen) {
       //①如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++
       if (j == -1 || s.charAt(i) == p.charAt(j)) {
           i++;
           j++;
       } else {
           //②如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]
           //next[j]即爲j所對應的next值
           j = next[j];
       }
   }
   if (j == pLen) {
       return i - j;
   } else {
       return -1;
   }
}

4. next數組優化

/**
* Table building: O(m)
* 優化的next數組
* @param p 匹配串
* @return
*/
private static int[] getNext2(String p) {
   int len = p.length();
   int[] next = new int[len];
   next[0] = -1;
   int i = 0, k = -1;
   while (i < len - 1) {
       // p[k]表示前綴,p[i]表示後綴
       if (k == -1 || p.charAt(i) == p.charAt(k)) {
           ++k;
           ++i;
           if (p.charAt(i) != p.charAt(k)) {
               next[i] = k;
           } else {
               // 因爲不能出現p[i] = p[next[i]],所以當出現時需要繼續遞歸,k = next[k] = next[next[k]]
               next[i] = next[k];
           }
       } else {
           k = next[k];
       }
   }
   return next;
}

參考

  1. 從頭到尾徹底理解KMP
  2. KMP算法
  3. KMP Algorithm for Pattern Searching
  4. Knuth–Morris–Pratt algorithm
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章