原創博客,轉載請註明http://my.oschina.net/BreathL/blog/137916 java
子字符串查找,是程序設計的一個基本且廣泛的問題。一般狀況下子字符串查找不須要特別的設計,一是因爲執行的次數很少,二是查找字符串通常也較短,因此不會造成性能的瓶頸;但若是你的程序裏有大量的查找或長字符串的子串查找,也許就須要考慮特別的設計已保證程序的效率。 c++
大部分語言默認的子字符串查找都是使用的暴力查找算法,思路很簡單,就是逐個取出待查找的字符串的字節,而後依次和被包含的子字符串的字節比較,從以第一個開始,若相等,則各自取下一個繼續比較;若不相等,則待查找的字符串回退回去到起始比較處的下一個字節,而子字符串從頭開始取,而後以此循環的比較。 算法
它的算法複雜度是 N*M 這個量級的,但有個問題是:當匹配失敗後,待查找的字符串回退回去到起始比較處的下一個字節,而子字符串從頭開始取時,緊接着的幾步比較多是多餘的計算。由於前X已經匹配上了,說明臨近X個字節已知了,那就能夠根據已知的狀況去掉一些重複的比較,這就KMP子字符串查找算法的優化原理。這麼說可能有些模糊,舉個例子: 數組
帶查找字符串: F Y Y Y Y U H N Z Y Y Y Y 目標字符串: F Y Y Y Y M 性能
第1次比較,到M是發現不匹配了,這時咱們已經知道了帶查找字符串前5個字符是什麼,因此經過必定的規律咱們能夠直接產生第7次比較,去掉第2~第6次的多餘的計算,由此提升效率。 優化
1. 待查找字符串,不回退,繼續取Next進行匹配。[暴力算法:回退起始匹配的Next] this
2. 包含的字符串,根據當前這個不匹配字節,來肯定,回退帶那個字節。[暴力算法:回退到開始字節] spa
爲了更好的實現上述優化的邏輯,KMP子字符串算法的整個設計思路以下: .net
1. 經過包含字符串,構造出一個二維的字典,兩個key對應一個值,這兩個key:1是對應全部的字節c,2是對應包含字符串字節位置(index);而值是下一個要去比較的包含字符串中字節的位置。 設計
2. 查找字符串,逐個取出字節,放到上述構建的那個二維字典中;取出的字節對應到第一個key,第二個key是包含字符串的index,初始時是0。這樣就獲得下一個包含字符串中須要比較的字節的位置;用這個位置更新第二個Key,同時取出查找字符串的Next 更新第一個Key;不斷循環。
我記得小時候有玩過一種遊戲(記不清名字了),跟這個很像:首選,遊戲者選擇一堆本身以爲對遊戲有利的條件;來到第一個格子,格子裏寫着:[若是知足XXX條件請直接到第六格、若是知足YYY條件請原地不動,若是知足MMM條件繼續前進一格],知足MMM條件的我顛兒顛兒的來到第二格,第二個格子一樣寫着不一樣的條件,到哪到哪。跟KMP的思路真是一模一樣;二維字典中的這第一個Key就是那些條件,第二個Key就是你當前所處的格子,值就是要去的下一個格子。我猜算法就是設計者玩這遊戲時想出的,絕對是,嗯。
由上述的KMP子字符串查找算法,咱們能夠清晰的看出只要這個二維字典一旦構造好,其餘的邏輯只是經過這個字典去查找,邏輯就比較簡單了。構造二維字典的目的也已經明確,存儲的是下一次比較的位置。說白了就是一次跳轉,這裏細說下這個跳轉,可分爲3種類別,分別實現這3種類別的邏輯,其實也就實現了二維字典的構造邏輯,這樣看代碼也會很清晰。
1 . 首選,匹配對了,即第一個Key的字符與第二個Key(即包含字符串的index)所對應的字符同樣,這時的跳轉就是下一個(index=index+1)。
2 . 匹配失敗,且無前綴,跳轉至起始位置,即index=0。
3 . 匹配失敗,有前綴,跳轉至前綴結束位置的下一個。
這裏所指的前綴,是前綴匹配,定義很簡單,這裏不細講了,舉個例子吧。
好比上圖中,匹配到第五個位置了,即紅色箭頭的比對,發現不匹配了,因而跳轉;因爲存在前綴,藍色方框對,因而 包含字符串 跳轉至 藍框結束後面的那個B 開始下一個的比較(查找字符串始終是往下一個跳轉),右圖紅框是匹配失敗後的起始的比較。
二維字典,通常用一個二維數組表示,dfa[][],它構造出來大體能夠想象成這麼個樣子,行是第一個Key,列是第二個Key:
完整的程序以下(代碼來自《算法》第四版):
package com.kmp; public class KMP { private String pat; private int[][] dfa; //二維字典 public KMP(String pat){ this.pat = pat; int M = pat.length(); int R = 256; //二維字典的構造 dfa = new int[R][M]; dfa[pat.charAt(0)][0] = 1; for (int X = 0, j = 1; j< M; j++){ //複製前面某一列的因此跳轉,至當前列,這某一列可視爲參照列 for( int c = 0; c < R; c++) dfa[c][j] = dfa[c][X]; dfa[pat.charAt(j)][j] = j + 1; //第j個字節,恰好在第j個位置上,說明匹配成功 X = dfa[pat.charAt(j)][X]; //更新參照列的位置 } } public int find(String content){ // 查找邏輯,相似遊戲 int i, j; int N = content.length(); int M = pat.length(); for (i = 0, j = 0; i<N && j<M; i++){ j = dfa[pat.charAt(i)][j]; } if (j == M) return i - M; //找到 else return M; } }
在二維字典的構造邏輯裏面,有個參照列的思路,它的原理是,一列即第二個Key對應全部字節的跳轉值是一種狀態,這種狀態是能夠保存並傳遞給下面某一列;具體的邏輯能夠簡單理解爲,重複了(即在中間出現了前綴),那麼程序裏的X,就會產生變化,從而是參照列改變。最開始都是參照第0列的;最後若是再也不繼續重複(即前綴中斷),X又會變回成0,從新用第0列做爲參照。