1. 前文回顧
在字符串算法—字典樹(Tries)中,我們實現了在一堆字符串中尋找某個字符串的高效算法。但如果要從一段字符中,尋找某個字符串呢?
我們可以用字符串算法—字符串排序(下篇)中的后綴排序法(suffix arrays)來尋找關鍵詞,但它消耗的內存有點大(畢竟要建一個超大的數組)。
為了解決這個問題,本文將介紹KMP算法(Knuth-Morris-Pratt)和BM算法(Boyer-Moore)。
2. KMP算法(Knuth-Morris-Pratt)
簡單介紹一下問題:
現有一段字符:AABACAABABACAA
問:ABABAC是否在這段字符里,如果在,在哪里?
從解決這個問題的過程中了解KMP算法:
左上角那個表格是KMP算法要用到的輔助表格,最下面的那個圖是這個表格的圖像化(方便理解用),右上角的圖為題目中的那段字符。
輔助表格如何建立,我們等下再介紹,我們先直接用。輔助表格中的字符是題目要我們找的字符串。
為了方便講解,我們命名題目中的那段字符為字符串S;要尋找的字符為字符串G;即:
字符串S: AABACAABABACAA
字符串G: ABABAC
首先,我們對比S和G的第一個字符,處於輔助表格的第0階段:
由於字符串S只有A,B,C三種字母,所以輔助表格只考慮了A,B,C三種情況。
處於第0階段時,如果遇見的是A,則前往第1階段;如果遇見的是B或者C,則停留在第0階段;
我們這里遇見的是S的第一個字符A,故前往第1階段:
然后我們來看S的下一個字符A,
處於第1階段時,如果遇見的是A,則停留在第1階段;如果遇見的是B,則前往第2階段;如果遇見的是C,則前往第0階段;
現在,我們遇到的是A,故停留在第1階段:
然后我們來看S的下一個字符B,處於第1階段遇見B,前往第2階段:(如果有字符已匹配上,則用綠色表示)
處於第2階段時,如果遇見的是A,則前往第3階段;如果遇見的是B或C,則前往第0階段
我們來看S的下一個字符A,故前往第3階段:
處於第3階段時,如果遇見的是A,則前往第1階段;如果遇見的是B,則前往第4階段;如果遇見的是C,則前往第0階段;
我們來看S的下一個字符C,故前往第0階段:
處於第0階段時,如果遇見的是A,則前往第1階段;如果遇見的是B或者C,則停留在第0階段;
我們這里遇見的是S的下一個字符A,故前往第1階段:
處於第1階段時,如果遇見的是A,則停留在第1階段;如果遇見的是B,則前往第2階段;如果遇見的是C,則前往第0階段;
現在,我們遇到的是S的下一個字符A,故停留在第1階段:
處於第1階段時,如果遇見的是A,則停留在第1階段;如果遇見的是B,則前往第2階段;如果遇見的是C,則前往第0階段;
我們遇到的是S的下一個字符B,故前往第2階段:
處於第2階段時,如果遇見的是A,則前往第3階段;如果遇見的是B或C,則前往第0階段
我們來看S的下一個字符A,故前往第3階段:
處於第3階段時,如果遇見的是A,則前往第1階段;如果遇見的是B,則前往第4階段;如果遇見的是C,則前往第0階段;
我們來看S的下一個字符B,故前往第4階段:
處於第4階段時,如果遇見的是A,則前往第5階段;如果遇見的是B或C,則前往第0階段
我們來看S的下一個字符A,故前往第5階段:
處於第5階段時,如果遇見的是A,則前往第1階段;如果遇見的是B,則前往第4階段;如果遇見的是C,則前往第6階段;
我們來看S的下一個字符C,故前往第6階段:
第6階段就是最終階段了,來到這個階段,說明已經找到字符串G了,至於G在字符串S的什么位置,這個容易求:
由於我們是逐個檢查字符串S的,(從int i=0開始逐漸遞增)所以我們是知道正在檢查第幾個字符的(i)。
int T= i-字符串G的長度+1; T就是字符串G所處位置,在本例子中,T=6,即字符串G在字符串S的第6個字符處。
如果我們需要知道某個字符串在某段字符中出現過多少次,分別在哪,則可以在每次找到此字符串時,重新回到第0階段,繼續尋找下去。
在本例中,任務完成了,算法結束。
順帶一提:所謂的第幾階段就是有幾個字符已經匹配上了。例如處於第三階段時,字符串G和字符串S已經匹配上了3個字符。
現在開始討論如何建立輔助表格:
首先最簡單的就是每個階段都遇到了正確的字符,即:
我們要查找的字符串G為ABABAC,第0階段遇到A;第1階段遇到B;第2階段遇到A;第3階段遇到B;第4階段遇到A;第5階段遇到C;那么每個階段都會前往下一個階段:
當我們遇到的是不正確的字符,該怎么辦呢?這里新增一個整數型變量int X=0;這個X將起輔助作用。
首先第0階段,只有遇到正確的字符才會前進,否則停留在原地,故:
然后到第1階段,當我們在第X階段時(X=0),遇到A會前往第1階段;遇到C會停留在第0階段。把這個結果填進第1階段:(圖中標紅的是第X階段)
然后更新X:現在在第1階段,第1階段的字符為B,在第X階段(X=0)遇到B會停留在第0階段,故X=0,X值沒改變。
然后到第2階段,當我們在第X階段時(X=0),遇到B會停留在第0階段;遇到C會停留在第0階段。把這個結果填進第2階段:
然后更新X:現在在第2階段,第2階段的字符為A,在第X階段(X=0)遇到A會前往第1階段,故X=1。
然后到第3階段,當我們在第X階段時(X=1),遇到A會前往第1階段;遇到C會前往第0階段。把這個結果填進第3階段:
然后更新X:現在在第3階段,第3階段的字符為B,在第X階段(X=1)遇到B會前往第2階段,故X=2。
然后到第4階段,當我們在第X階段時(X=2),遇到B會前往第0階段;遇到C會前往第0階段。把這個結果填進第4階段:
然后更新X:現在在第4階段,第4階段的字符為A,在第X階段(X=2)遇到A會前往第3階段,故X=3。
然后到第5階段,當我們在第X階段時(X=3),遇到A會前往第1階段;遇到B會前往第4階段。把這個結果填進第5階段:
這樣輔助表格就做好了。
輔助表格的制作過程加上一開始介紹的尋找過程就是完整的KMP算法了。
實現代碼
建立表格:
尋找字符:
3. KMP算法效率
Brute force(暴風算法)是種蠻力算法,它把要查找的字符串與原字符串的第一個字符開始一一對比,如果發現不匹配的字符,則從下一個字符開始一一個對比。如此類推,直到找到了該字符串或原字符串所有字符都對比完(找不到該字符串的情況)為止。
由於此算法效率低下,這里沒有細講。
圖中N為原字符串所含字符數量;M為要找的字符串所含字符數量,R為字符串中可能出現的字符種類數量,根據下圖選擇:
4. BM算法(Boyer-Moore)
百度了一下:在用於查找子字符串的算法當中,BM(Boyer-Moore)算法是目前被認為最高效的字符串搜索算法,它由Bob Boyer和J Strother Moore設計於1977年。 一般情況下,比KMP算法快3-5倍。
這個算法牛的地方在於要查找的字符串越長,搜索效率越高。
從例子入手:
如上圖,我們把所有的字符的值定為-1,要查找的字符串needle含有4個不同的字符,我們根據它們所處位置給它們賦值:right[N]=0; right[E]=5; right[D]=3; right[L]=4;其中E出現了3次,我們取其中的最大值。
新增整數變量 int i=0; int j=0; 原字符串長度N=24; 查找的字符串長度M=6;
首先。j=M-1;即j=5;從第j個字符開始比較:
比較結果:不相等。
然后要決定我們可以跳幾個字符:原字符串的第5個字符為N,right[N]=0; j-right[N]=5-0=5; 故我們可以跳5個字符:i +=5; i=5;
j+i=5+5=10;比較原字符的第10個字符:
比較結果:不相等。
然后要決定我們可以跳幾個字符:原字符串的第10個字符為S,right[S]=-1; j-right[S]=5-(-1)=6; 故我們可以跳6個字符(因為s不在要查找的字符串里,所以可以把這整段跳過去):i +=6; i=11;
j+i=5+11=16;比較原字符的第16個字符:
比較結果:相等。比較前一個字符, j--; j=5-1=4:
計較結果:相等。
然后要決定我們可以跳幾個字符:原字符串的第15個字符為N,right[N]=0; j-right[N]=4-0=4; 故我們可以跳4個字符:i +=4; i=15, j=M; j=5;
j+i=5+15=20;比較原字符的第20個字符:
比較結果:相等。比較前一個字符, j--; j=5-1=4:
比較結果:相等。比較前一個字符, j--; j=4-1=3:(由於接下來的結果都是相等,故省略中間過程,直接跳到j=0)
比較結果:相等。由於j=0;再減下去就是負數了,算法也在這里結束。要查找的字符串在原字符的第i個字符處(i=15)。
另外,值得一提的是,請看以下情況:
此時,j=3, 比較結果不相同。
然后要決定我們可以跳幾個字符:原字符串的第18個字符為E,right[E]=5; j-right[E]=3-5=-2; 跳的步數為負數,我們總不能往回跳吧!故當跳的步數為負時,我們只往前跳一個字符:
此時如果再跳,就要跳出字符串之外了,故當i>N-M時,我們停止算法,判斷結果為沒找到該字符。
當我們的要找的字符串越長時,我們可能能跳的字符數也越多,算法越快。(為什么是可能?因為當原字符串的字符都出現在要查找的字符串時,是沒辦法跳M個字符的。)
實現代碼:
5. BM算法效率
圖中N為原字符串所含字符數量;M為要找的字符串所含字符數量,R為字符串中可能出現的字符種類數量。
但是,如果遇到最壞情況:
在這種情況下,BM算法跳不了,只能一步一步往前比較。此時就等於是Brute force(暴風算法)了。