看了好久才看懂。。學校教材還有錯誤,看了別人的博客+別的學校的教材才看懂。。
下面是孤~影的博客內容,最后還會放教材上的圖和自己寫的代碼實現。其實本來是想自己寫一篇博客的,奈何最近事情實在是太多了。。以后有時間復習到這邊的時候自己寫一篇吧~
看了他的博客看懂了很多,鏈接:詳解KMP算法
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
什么是KMP算法:
KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同時發現的。其中第一位就是《計算機程序設計藝術》的作者!!
KMP算法要解決的問題就是在字符串(也叫主串)中的模式(pattern)定位問題。說簡單點就是我們平時常說的關鍵字搜索。模式串就是關鍵字(接下來稱它為P),如果它在一個主串(接下來稱為T)中出現,就返回它的具體位置,否則返回-1(常用手段)。
首先,對於這個問題有一個很單純的想法:從左到右一個個匹配,如果這個過程中有某個字符不匹配,就跳回去,將模式串向右移動一位。這有什么難的?
我們可以這樣初始化:
之后我們只需要比較i指針指向的字符和j指針指向的字符是否一致。如果一致就都向后移動,如果不一致,如下圖:
A和E不相等,那就把i指針移回第1位(假設下標從0開始),j移動到模式串的第0位,然后又重新開始這個步驟:
基於這個想法我們可以得到以下的程序:
1 /** 2 3 * 暴力破解法 4 5 * @param ts 主串 6 7 * @param ps 模式串 8 9 * @return 如果找到,返回在主串中第一個字符出現的下標,否則為-1 10 11 */ 12 13 public static int bf(String ts, String ps) { 14 15 char[] t = ts.toCharArray(); 16 17 char[] p = ps.toCharArray(); 18 19 int i = 0; // 主串的位置 20 21 int j = 0; // 模式串的位置 22 23 while (i < t.length && j < p.length) { 24 25 if (t[i] == p[j]) { // 當兩個字符相同,就比較下一個 26 27 i++; 28 29 j++; 30 31 } else { 32 33 i = i - j + 1; // 一旦不匹配,i后退 34 35 j = 0; // j歸0 36 37 } 38 39 } 40 41 if (j == p.length) { 42 43 return i - j; 44 45 } else { 46 47 return -1; 48 49 } 50 51 }
上面的程序是沒有問題的,但不夠好!(想起我高中時候數字老師的一句話:我不能說你錯,只能說你不對~~~)
如果是人為來尋找的話,肯定不會再把i移動回第1位,因為主串匹配失敗的位置前面除了第一個A之外再也沒有A了,我們為什么能知道主串前面只有一個A?因為我們已經知道前面三個字符都是匹配的!(這很重要)。移動過去肯定也是不匹配的!有一個想法,i可以不動,我們只需要移動j即可,如下圖:
上面的這種情況還是比較理想的情況,我們最多也就多比較了再次。但假如是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比較到最后一個才知道不匹配,然后i回溯,這個的效率是顯然是最低的。
大牛們是無法忍受“暴力破解”這種低效的手段的,於是他們三個研究出了KMP算法。其思想就如同我們上邊所看到的一樣:“利用已經部分匹配這個有效信息,保持i指針不回溯,通過修改j指針,讓模式串盡量地移動到有效的位置。”
所以,整個KMP的重點就在於當某一個字符與主串不匹配時,我們應該知道j指針要移動到哪?
接下來我們自己來發現j的移動規律:
如圖:C和D不匹配了,我們要把j移動到哪?顯然是第1位。為什么?因為前面有一個A相同啊:
如下圖也是一樣的情況:
可以把j指針移動到第2位,因為前面有兩個字母是一樣的:
至此我們可以大概看出一點端倪,當匹配失敗時,j要移動的下一個位置k。存在着這樣的性質:最前面的k個字符和j之前的最后k個字符是一樣的。
如果用數學公式來表示是這樣的
P[0 ~ k-1] == P[j-k ~ j-1]
這個相當重要,如果覺得不好記的話,可以通過下圖來理解:
弄明白了這個就應該可能明白為什么可以直接將j移動到k位置了。
因為:
當T[i] != P[j]時
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]
公式很無聊,能看明白就行了,不需要記住。
這一段只是為了證明我們為什么可以直接將j移動到k而無須再比較前面的k個字符。
好,接下來就是重點了,怎么求這個(這些)k呢?因為在P的每一個位置都可能發生不匹配,也就是說我們要計算每一個位置j對應的k,所以用一個數組next來保存,next[j] = k,表示當T[i] != P[j]時,j指針的下一個位置。
很多教材或博文在這個地方都是講得比較含糊或是根本就一筆帶過,甚至就是貼一段代碼上來,為什么是這樣求?怎么可以這樣求?根本就沒有說清楚。而這里恰恰是整個算法最關鍵的地方。
1 public static int[] getNext(String ps) { 2 3 char[] p = ps.toCharArray(); 4 5 int[] next = new int[p.length]; 6 7 next[0] = -1; 8 9 int j = 0; 10 11 int k = -1; 12 13 while (j < p.length - 1) { 14 15 if (k == -1 || p[j] == p[k]) { 16 17 next[++j] = ++k; 18 19 } else { 20 21 k = next[k]; 22 23 } 24 25 } 26 27 return next; 28 29 }
這個版本的求next數組的算法應該是流傳最廣泛的,代碼是很簡潔。可是真的很讓人摸不到頭腦,它這樣計算的依據到底是什么?
好,先把這個放一邊,我們自己來推導思路,現在要始終記住一點,next[j]的值(也就是k)表示,當P[j] != T[i]時,j指針的下一步移動位置。
先來看第一個:當j為0時,如果這時候不匹配,怎么辦?
像上圖這種情況,j已經在最左邊了,不可能再移動了,這時候要應該是i指針后移。所以在代碼中才會有next[0] = -1;這個初始化。
如果是當j為1的時候呢?
顯然,j指針一定是后移到0位置的。因為它前面也就只有這一個位置了~~~
下面這個是最重要的,請看如下圖:
請仔細對比這兩個圖。
我們發現一個規律:
當P[k] == P[j]時,
有next[j+1] == next[j] + 1
其實這個是可以證明的:
因為在P[j]之前已經有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
這時候現有P[k] == P[j],我們是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
這里的公式不是很好懂,還是看圖會容易理解些。
那如果P[k] != P[j]呢?比如下圖所示:
像這種情況,如果你從代碼上看應該是這一句:k = next[k];為什么是這樣子?你看下面應該就明白了。
現在你應該知道為什么要k = next[k]了吧!像上邊的例子,我們已經不可能找到[ A,B,A,B ]這個最長的后綴串了,但我們還是可能找到[ A,B ]、[ B ]這樣的前綴串的。所以這個過程像不像在定位[ A,B,A,C ]這個串,當C和主串不一樣了(也就是k位置不一樣了),那當然是把指針移動到next[k]啦。
有了next數組之后就一切好辦了,我們可以動手寫KMP算法了:
1 public static int KMP(String ts, String ps) { 2 3 char[] t = ts.toCharArray(); 4 5 char[] p = ps.toCharArray(); 6 7 int i = 0; // 主串的位置 8 9 int j = 0; // 模式串的位置 10 11 int[] next = getNext(ps); 12 13 while (i < t.length && j < p.length) { 14 15 if (j == -1 || t[i] == p[j]) { // 當j為-1時,要移動的是i,當然j也要歸0 16 17 i++; 18 19 j++; 20 21 } else { 22 23 // i不需要回溯了 24 25 // i = i - j + 1; 26 27 j = next[j]; // j回到指定位置 28 29 } 30 31 } 32 33 if (j == p.length) { 34 35 return i - j; 36 37 } else { 38 39 return -1; 40 41 } 42 43 }
和暴力破解相比,就改動了4個地方。其中最主要的一點就是,i不需要回溯了。
最后,來看一下上邊的算法存在的缺陷。來看第一個例子:
顯然,當我們上邊的算法得到的next數組應該是[ -1,0,0,1 ]
所以下一步我們應該是把j移動到第1個元素咯:
不難發現,這一步是完全沒有意義的。因為后面的B已經不匹配了,那前面的B也一定是不匹配的,同樣的情況其實還發生在第2個元素A上。
顯然,發生問題的原因在於P[j] == P[next[j]]。
所以我們也只需要添加一個判斷條件即可:
public static int[] getNext(String ps) { char[] p = ps.toCharArray(); int[] next = new int[p.length]; next[0] = -1; int j = 0; int k = -1; while (j < p.length - 1) { if (k == -1 || p[j] == p[k]) { if (p[++j] == p[++k]) { // 當兩個字符相等時要跳過 next[j] = next[k]; } else { next[j] = k; } } else { k = next[k]; } } return next; }
好了,至此。KMP算法也結束了。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
看的過程中還有些地方不明白的,借了下別人的教材翻了翻,結合起來多看幾遍就能看懂了,放下圖:
下面是我自己的java實現代碼:

1 public class FindString { 2 public static int[] next = new int[4]; 3 public static void main(String[] args) { 4 // System.out.println(indexBF("abcabab".toCharArray(),"abcc".toCharArray())); 5 getNext("bbab".toCharArray(), next); 6 for(int i:next) { 7 System.out.print(i+" "); 8 } 9 } 10 11 //KMP模式匹配算法 12 //從串s的第pos個字符開始查找首次與串t相等的子串 13 public static int KMP(char s[],char p[],int pos) { 14 int i = pos,j = 0; 15 while(i<s.length && j<p.length) { 16 if(j==0 || s[i]==p[j]) { 17 i++;j++; 18 }else { 19 j = next[j]; 20 } 21 } 22 if(j == p.length) return i-j; 23 else return -1; 24 } 25 26 public static void getNext(char[] p,int[] next) { 27 next[0] = -1; 28 int j = 0,k = -1; //k實際上就是當前next[j]的值 29 //通過next[j]遞推來求next[j+1] 30 while(j<p.length-1) { 31 if(k == -1 || p[j]==p[k]) { //如果是k=-1的情況,即next[j+1]=0; 32 // next[++j] = ++k; 33 if(p[++j] == p[++k]) { //如果兩個字符串相等要跳過 34 next[j] = next[k]; 35 }else { 36 next[j] = k; 37 } 38 }else { 39 k = next[k]; 40 } 41 } 42 } 43 44 //bf:brute force 暴力破解法 s:主串 p:模式串 45 public static int indexBF(char s[],char p[]) { 46 int i = 0,j = 0; 47 while(i<s.length && j<p.length) { 48 if(s[i] == p[j]) { 49 i++;j++; 50 }else { 51 i = i-j+1; 52 j = 0; 53 } 54 } 55 if(j == p.length) { 56 return i-j; 57 }else { 58 return -1; 59 } 60 } 61 62 }
說真的,看了一天很多次都快炸了,但是越這樣越看不進去。。就像大家都在說的一樣,學習切忌浮躁,一定要沉心靜氣!