任何一種問題,咱們都習慣先寫出暴力作法,而後再去想如何優化。對於字符串匹配也是如此,話很少說,直接上代碼,暴力遍歷比較。c++
for (int i = 0; i < n; i ++ ) { bool flag = true; for (int j = 0; j < n-m+1; j ++ ) { if (text[i + j ] != pattern[j]) { flag=false; break; } } if(flag) return i; }
雖說暴力作法一直是效率低下的代言詞,但對於日常使用來講,這種作法就已經夠用了。由於在實際開發中,大部分狀況下的主串和模式串的長度都不會太長。而且它思路清晰簡單,出問題也好修復。實際上通常 string 的查找函數就是這種作法。算法
可是也不能就會這一招就阿尼陀佛,萬事大吉了。總有須要優化的時候,計算機之因此在現代社會有着舉足輕重的地位,就在於人在不斷思考,並優化它的執行方式。缺乏了人的思考,它只是一坨笨的不能再笨的鐵而已。數組
樸素作法雖然簡單,但主串與模式串的指針都要來回移動。咱們去書上找東西通常都是一眼掃過去,有沒有什麼辦法可讓主串的指針不往回走,下降比較的趟數?函數
在上面這個例子中,很明顯更快的作法是在第一次匹配失敗以後,跳過第二次匹配,進行第三次匹配。也就是說,應該向後移動兩格而不是一格。KMP的思想就在於利用已知信息,不把"搜索位置"移回已經比較過的位置,繼續向後移。性能
咱們能夠利用模式串自身的特色,利用已經匹配過的結果來減小枚舉過程,跳過不可能成功的比較,加速匹配過程。畫個抽象點的圖:優化
在主串指針不日後退的前提下, 咱們的模式串指針最多回退到哪裏就能夠繼續進行匹配呢?3d
如上圖,回退以後的字符串確定是要能與主串匹配成功的。也就是說它與以前的模式串有着交集。從圖中能夠看到,P串的後綴與P'的前綴相同。那個這個問題:在已有N個匹配成功的結果下,第N+1個字符匹配失敗時,模式串應該退回到第?個匹配成功的結果下?也就轉換成了:模式串的各個字串中,能使先後綴相等的最大長度是多少?指針
咱們用next數組來存儲回退到的下標code
雖說1那裏應該等於0,但其實這時候已經退無可退了。因此應該退出循環,讓主串指針向前走一位,開始新一輪比較。blog
利用這個next數組,好比咱們比較到下標3的時候發現不匹配,不要緊,咱們退一步。退一步以後就到了下標1(回退看的next應該是已經匹配成功的那個,也就是看下標2存的是幾),若是這時候還不匹配,那咱們就會退回到-1,也就是說,這輪匹配失敗了,主串能夠往前走了。
看看代碼就清楚了
int strStr(string haystack, string needle) { if (!needle.size()) return 0; if (!haystack.size()) return -1; vector<int> next(needle.size()); next[0] = -1; //求next數組 for (int i = 1, j = -1; i < needle.size(); i++) { while (j >= 0 && needle[i]!=needle[j+1]) { //不匹配就退一步看看 j = next[j]; } if (needle[i] == needle[j + 1]) { //匹配成功就繼續日後走 //看看還能不能匹配成功 j++; } next[i] = j; } //開始匹配 for (int i = 0, j = -1; i < haystack.size(); i++) { while (j != -1 && haystack[i] != needle[j+1]) { //不匹配咱就回退 j = next[j]; } if (haystack[i] == needle[j+1]) { j++; } if (j == needle.size() - 1) { return i-j; } } return -1; }
雖然KMP比較出名,但其實只是由於它比較難懂而已。在效率上有不少的算法都比它要好。如今介紹的BM算法,其效率就要比KMP好上3到4倍。
BM算法包含兩部分:壞字符規則和好後綴規則
BM算法與咱們日常接觸的字符串比較方法不一樣,它是按模式串從大到小的順序,倒着比的。這樣作也是有好處的,起碼直觀上是這樣感受的。就像作算數選擇題,出卷老師爲了讓你花的時間久一點,故意把正確答案放到C跟D上。因此聰明點的作法應該是先算C跟D。這跟這個比較方法有點相似。
考慮下面這張圖:
別忘了咱們是從模式串最大的開始日後匹配,因此這裏先比較了C和D。這個時候,有意思的來了,這個 D
在模式串中就沒有出現過,是一個壞字符
,有它在的字串可能不匹配。
惹不起還躲不起嗎?逃之夭夭,模式串直接移動五位,從新匹配。一下移動這麼多,是否是特別的爽?
但你千萬不要覺得只有沒在模式串中出現過的才叫壞字符
,實際上這個只是從後往前第一個不匹配的字符。一旦發生不匹配,壞字符規則的作法是模式串指針繼續往回走,找到第一個與其匹配的字符中止,而後再繼續新一輪的匹配。
也就是說,移動的距離等於:當前模式串的下標(Si) - 往回走找到的第一個與當前不匹配字符匹配的下標(Xi)
。若是沒有找到,則減數爲-1。
這麼一想,好像用壞字符規則就萬事大吉了。但由於實際代碼中,咱們不會每次不匹配都會往前找,那樣太耗費時間,取而代之的是使用散列表紀錄不一樣字符在模式串中「最後出現的位置」,並非 Si 的位置往前查找的第一個位置,因此可能會出現 Xi 大於 Si 的狀況。好比上圖的例子,主串"ABABDABABCAB",模式串"ABABC"當從左開始數的一個B不匹配時,找到的A的下標是最後一次在模式串中出現的下標(也就是最後一個a的位置,比A大)。這時候,模式串非但不往前滑動,還回退了。爲了解決這個問題,咱們須要好後綴
來幫忙。(實際上,兩個規則均可以獨立使用,若是壞字符你是往回遍歷而不是保存在散列表裏面的話)
當遇到上圖的狀況時,咱們依然能夠用壞字符規則來移動,但此次讓咱們來看看好後綴是如何工做的?
咱們把在主串中已經匹配成功的字符串用 u
標記,如今要作的是找到模式串中與其匹配的u*
若是找到了,那模式串就滑動到使得u*
與u對齊的位置。若是不匹配,那麼逃之夭夭,直接移動一個模式串長度的位置。雖然一次移動那麼可能是很爽,但這樣作有可能錯過能夠匹配的狀況。
實際上這裏應該跟KMP同樣,若是模式串中前綴與好後綴的後綴相同,那麼就移動相應位置使其匹配。
兩個規則你都知道了,而且他們均可以獨立使用,那麼到底該選哪個呢?
如前面所說,壞字符若是每次都在模式串中遍歷的話,會對性能形成影響。那麼有沒有什麼高效的辦法代替遍歷呢?
咱們能夠將模式串中的每一個字符及其下標都存到散列表中,這樣就能快速的找到壞字符在模式串的對應下標了。
先打好壞字符規則
private: static const int SIZE = 256; //求bc vector<int> getBC(string pattern) { vector<int> bc(SIZE,-1); for (int i = 0; i < pattern.size(); i++) { int ascii = (int)pattern[i]; bc[ascii] = i; } return bc; } int strStr(string haystack, string needle) { if (needle.size() == 0) return 0; vector<int> bc(SIZE); bc = getBC(needle); int i = 0; while (i <= haystack.size() - needle.size()) { int j; //別忘了,BM是從後往前匹配哦 for (j = needle.size() - 1; j >= 0; j--) { //不等於咱就要去找滑動位置了 if (haystack[i + j] != needle[j]) break; } if (j < 0) { //這是匹配成功!咱們是從後往前的!!! return i; } //找到模式串中最近的壞字符 i = i + j - bc[(int)haystack[i + j]]; } return -1; }
好後綴稍微麻煩一點,與KMP相似,咱們用一個int數組suffix來保存。其下標表示後綴子串的長度。讓咱們來明確一下要乾的事:在好後綴的後綴子串中,查找最長的、能跟模式串前綴子串匹配的後綴子串。因此存儲的應該是在模式串中跟好後綴u
相匹配的子串u*
的起始下標值。
等等!咱們好像漏了什麼?在模式串中查找跟好後綴匹配的另外一個子串,而且在好後綴的後綴子串中,查找最長能跟模式串前綴子串匹配的後綴子串。
suffix數組只能完成前半段話,那前綴子串的問題該如何解決呢?咱們只能是再開一個bool數組prefix 來作這件事了。
求這兩個數組的方法也十分討巧,紀錄公共後綴子串u*
的起始下標爲j,若是j==0,說明是前綴字串(由於已經走到盡頭了),prefix爲true。代碼實現是下面這樣:
//好後綴 void genGS(string pattern) { int len = pattern.size(); for (int i = 0; i < len; i++) { suffix.push_back(-1); prefix.push_back(false); } for (int i = 0; i < len - 1; i++) { int j = i; //公共後綴u*的長度 int k = 0; while (j >= 0 && pattern[j] == pattern[len - 1 - k]) { j--; k++; //保存的是在pattern中的起始下標 suffix[k] = j + 1; } if (j == -1) prefix[k] = true; } }
如今讓咱們來把兩個規則一塊兒用在字符串匹配上
private: static const int SIZE = 256; vector<int> bc; vector<int> suffix; vector<bool> prefix; //壞字符 void getBC(string pattern) { for (int i = 0; i < SIZE; i++) { bc.push_back(-1); } for (int i = 0; i < pattern.size(); i++) { int ascii = (int)pattern[i]; bc[ascii] = i; } } //好後綴 void genGS(string pattern) { int len = pattern.size(); for (int i = 0; i < len; i++) { suffix.push_back(-1); prefix.push_back(false); } for (int i = 0; i < len - 1; i++) { int j = i; //公共後綴u*的長度 int k = 0; while (j >= 0 && pattern[j] == pattern[len - 1 - k]) { j--; k++; //保存的是在pattern中的起始下標 suffix[k] = j + 1; } if (j == -1) prefix[k] = true; } } int moveByGS(int j, int len) { //好後綴長度 int k = len - 1 - j; if (suffix[k] != -1) return j - suffix[k] + 1; for (int i = j + 2; i < len; i++) { if (prefix[len = i]) return i; } return len; } int strStr(string haystack, string needle) { if (needle.size() == 0) return 0; getBC(needle); genGS(needle); int i = 0; while (i <= haystack.size() - needle.size()) { int j; //別忘了,BM是從後往前匹配哦 for (j = needle.size() - 1; j >= 0; j--) { //不等於咱就要去找滑動位置了 if (haystack[i + j] != needle[j]) break; } if (j < 0) { //這是匹配成功!咱們是從後往前的!!! return i; } //找到模式串中最近的壞字符 int x = j - bc[(int)haystack[i + j]]; int y = 0; //若是有好後綴 if (j < needle.size() - 1) { y = moveByGS(j,needle.size()); } i = i + max(x, y); } return -1; }