循序漸進,深入理解KMP演算法
KMP演算法是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同時發現的。其中第一位就是《計算機程式設計藝術》的作者!
KMP演算法要解決的問題就是在字串(也叫主串)中的模式(pattern)定位問題。說簡單點就是我們平時常說的關鍵字搜尋。模式串就是關鍵字(接下來稱它為P),如果它在一個主串(接下來稱為T)中出現,就返回它的具體位置,否則返回-1(常用手段)。
常規的暴力匹配演算法時間複雜度:O(m*n),而KMP演算法目的是儘快解決字串匹配問題,時間複雜度為O(m+n)。
1、暴力匹配演算法(腦補一下就好,不用看的)
首先,對於這個問題有一個很單純的想法:從左到右一個個匹配,如果這個過程中有某個字元不匹配,就跳回去,將模式串向右移動一位。這有什麼難的?
我們可以這樣初始化:
之後我們只需要比較i指標指向的字元和j指標指向的字元是否一致。如果一致就都向後移動,如果不一致,如下圖:
A和E不相等,那就把i指標移回第1位(假設下標從0開始),j移動到模式串的第0位,然後又重新開始這個步驟:
程式碼:
public class Demo{ public static int bf(String ts,String ps) { char[] t = ts.toCharArray(); char[] p = ps.toCharArray(); int i = 0; // 主串的位置 int j = 0; // 模式串的位置 while (i < t.length && j < p.length) { if (t[i] == p[j]) { // 當兩個字元相同,就比較下一個 i++; j++; }else { i = i - j + 1; // 一旦不匹配,i後退 j = 0; // j歸0 } } if (j == p.length) { return i - j; }else { return -1; } } }
2、KMP演算法(重點)
演算法的改進之處在於:每當一趟匹配過程中出現字元比較不相等時,指向主串的指標 i 不倒退,而是利用已經得到的“部分匹配”的結果,將模式串向右滑動儘可能遠的一段距離後,繼續比較。
例如:主串S:"acacbacbabca",模式串P:"acbab"
只進行了三趟比較,就匹配上了,是不是很厲害。我們可以注意到整個過程 i 是不往回走的,所以時間複雜度為O(n+m)。
使用這個演算法,需要解決兩個問題:
- 比較不成功時,模式串T向右滑動多少距離
- 滑動之後,模式串T的工作指標 j 的值
為了解決這兩個問題,我們需要引入一個數組PMT
我先解釋一下字串的字首和字尾。如果字串A和B,存在A=BS,其中S是任意的非空字串,那就稱B為A的字首。例如,”Harry”的字首包括{”H”, ”Ha”, ”Har”, ”Harr”},我們把所有字首組成的集合,稱為字串的字首集合。同樣可以定義字尾A=SB, 其中S是任意的非空字串,那就稱B為A的字尾,例如,”Potter”的字尾包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然後把所有後綴組成的集合,稱為字串的字尾集合。要注意的是,字串本身並不是自己的字尾。
有了這個定義,就可以說明PMT中的值的意義了。PMT中的值是字串的字首集合與字尾集合的交集中最長元素的長度。例如,對於”aba”,它的字首集合為{”a”, ”ab”},字尾 集合為{”ba”, ”a”}。兩個集合的交集為{”a”},那麼長度最長的元素就是字串”a”了,長 度為1,所以對於”aba”而言,它在PMT表中對應的值就是1。再比如,對於字串”ababa”,它的字首集合為{”a”, ”ab”, ”aba”, ”abab”},它的字尾集合為{”baba”, ”aba”, ”ba”, ”a”}, 兩個集合的交集為{”a”, ”aba”},其中最長的元素為”aba”,長度為3。
例如index=4時,考察字串”ababa”的最大相等k字首和k字尾,最大值為3:
字首串 |
字尾串 |
a |
a |
ab |
ba |
aba |
aba |
abab |
baba |
但是一般在程式設計中不用PMT,而是用next陣列計算。
為了程式設計的方便, 我們不直接使用PMT陣列,而是將PMT陣列向後偏移一位。我們把新得到的這個陣列稱為next陣列。下面給出根據next陣列進行字串匹配加速的字串匹配程式。其中要注意的一個技巧是,在把PMT進行向右偏移時,第0位的值,我們將其設成了-1,這只是為了程式設計的方便,並沒有其他的意義。
則next[j]表示: 前j-1位,字串的字首集合與字尾集合的交集中最長元素的長度。
我們來通過例子,觀察next陣列是如何解決,之前的兩個問題的:
- 比較不成功時,模式串T向右滑動多少距離
- 滑動之後,模式串T的工作指標 j 的值
當發生不相等的情況時( S[i] != T[j] ):此時next[6]=4,這個時候讓 S[6]要和T[4]比較(j = next[j]):
KMP程式碼:
//返回第一個匹配上的起始位置
public static int KMP(String text, String pattern) {
int i = 0,j = 0;
while(i<text.length() && j<pattern.length()) {
if(j==-1 || text.charAt(i)==pattern.charAt(j)) {//匹配上了就繼續往下匹配
++i;
++j;
}else
j = next[j];//發生不相等的情況
}
if(j>=pattern.length())return i-pattern.length();//return i-j;
return -1;
}
那麼next陣列如何計算呢?我們可以將它看成是兩個相同串的匹配,初始位置差1位
next[j]表示: 前j-1位,字串的字首集合與字尾集合的交集中最長元素的長度。
next[2]算的是第0,1位,字首集合與字尾集合的交集中最長元素的長度。
next陣列計算程式碼,和KMP程式碼很像:
public static void getNext(String p, int []next)
{
next[0] = -1;
int i = 0, j = -1;
while (i < p.length())
{
if (j == -1 || p.charAt(i) == p.charAt(j))
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
彙總兩部分的程式碼:
public class KMP {
public static void getNext(String p, int []next)
{
next[0] = -1;
int i = 0, j = -1;
while (i < p.length())
{
if (j == -1 || p.charAt(i) == p.charAt(j))
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
//返回第一個匹配上的起始位置
public static int KMP(String text, String pattern , int next[]) {
int i = 0,j = 0;
while(i<text.length() && j<pattern.length()) {
if(j==-1 || text.charAt(i)==pattern.charAt(j)) {
++i;
++j;
}else
j = next[j];
}
if(j>=pattern.length())return i-pattern.length();//return i-j;
return -1;
}
public static void print(int []a) {
for(int i = 0 ; i < a.length ; i++) {
System.out.print(a[i]+" ");
}
}
public static void main(String[] args) {
String s = "abcabcabdabba";
String t = "abcabd";
int a[] = new int[s.length()+1];
getNext(s , a);
int index = KMP(s,t,a);//返回第一個匹配的位置
System.out.print(index);
}
}
參考資料: