字符串匹配算法
字符串匹配就是在主串A中查找模式串B,例如在主串a(chǎn)bababc中查找模式串a(chǎn)bc是否存在,記主串A的長度為n,模式串B的長度為m,n>=m。
BF算法
BF(Brute Force)算法,又叫暴力匹配算法或者樸素匹配算法,思路很簡單:在主串中取前下標(biāo)為[0,m-1]這m個(gè)字符的子串和模式串逐個(gè)字符逐個(gè)字符比較,如果完全一樣就結(jié)束并返回下標(biāo);如果有不一樣的,那么主串中的子串后移一位,主串中[1,m]這個(gè)子串和模式串繼續(xù)比較,… ,主串中[n-m,n-1]這個(gè)子串和模式串繼續(xù)比較。主串中長度為m的子串有n-m+1個(gè)。
?
最壞的情況下,在第一個(gè)for循環(huán)里,i 從0到n-m走滿共n-m+1次,第二個(gè)for循環(huán)里,j 從0到m-1走滿共m次,因此最壞的情況下時(shí)間復(fù)雜度為O(n*m),舉個(gè)例子,在bbbbbbf中查找bf,所有n-m+1個(gè)子串都要走完,并且每次和模式串比較都要比較m次,總共比較n-m+1次。
BF算法最大的優(yōu)點(diǎn)就是簡單,代碼不容易出錯(cuò),在主串和模式串的長度都不大的時(shí)候還是比較實(shí)用的。
RK算法
RK(Rabin-Karp)算法,是用兩個(gè)發(fā)明者的名字命名的。思路也比較簡單:對主串中n-m+1個(gè)子串求哈希值,模式串也求哈希值,然后比較子串的哈希值和模式串的哈希值,如果不相等證明不匹配,如果相等就匹配(在沒有哈希沖突的情況,沖突的情況后面會講)。
??這個(gè)算法對哈希函數(shù)的設(shè)計(jì)要求會高一點(diǎn),當(dāng)然最好就不存在哈希沖突,就會比較簡單,相等就匹配,不相等就不匹配。
??來看看這樣一個(gè)設(shè)計(jì):假設(shè)主串和模式串只有a-j這10個(gè)字母,我們可以直接將字符串映射成整數(shù)(a-j對應(yīng)十進(jìn)制0-9),例如bcd我們可以直接映射成bcd=1*10*10+2*10+3=123,這樣就不存在哈希沖突了。那如果現(xiàn)在是a-z這26個(gè)字母,我們可以用同樣的思路,但是使用26進(jìn)制,a-z對應(yīng)0-25:例如bcd=1*26*26+2*26+3=731。
??這個(gè)哈希函數(shù)是有規(guī)律的,當(dāng)前子串的哈希值hash[i]是可以根據(jù)上一個(gè)子串的哈希值hash[i-1]計(jì)算得到,來看看下面這個(gè)例子:
上面的子串a(chǎn)ba的起始坐標(biāo)為i-1,哈希值為hash[i-1],下面子串bab的起始坐標(biāo)為i,哈希值為hash[i],我們知道兩個(gè)子串中都有ba,但是下面的ba映射成的值要比上面的ba要大26倍,因?yàn)樗诘奈恢貌灰粯?#xff0c;下面的ba在最高位和第二位,下面的ba在第二位和第三位,那么我們將hash[i-1]乘26,上下子串ba的hash值都相同了,然后再減去上面子串的最高位a(注意此時(shí)的a也是乘多了個(gè)26的),最后加上下面子串的末尾的b即可。最終的計(jì)算結(jié)果就同上圖中的計(jì)算一樣,注意藍(lán)色框框的m即可,是m而不是m-1,因?yàn)閔ash[i-1]乘了26。
遍歷一次主串就可以求出所有子串的哈希值了,求一個(gè)子串的哈希值時(shí)間復(fù)雜度可以看做O(1),求所有子串的哈希值的時(shí)間復(fù)雜度為O(n)。模式串和子串的哈希值比較時(shí)間復(fù)雜度為O(1),共有n-m+1個(gè)子串即比較n-m+1次,所以時(shí)間復(fù)雜度為O(n)。所以RK算法的時(shí)間復(fù)雜度為O(n)。
如果模式串的長度m太大,字符不止26個(gè)字母,我們上面設(shè)計(jì)的算法可能會溢出整型數(shù)據(jù)的范圍,我們也可以設(shè)計(jì)別的哈希函數(shù):例如每種字符對應(yīng)一個(gè)小質(zhì)數(shù),然后將所有字符對應(yīng)的質(zhì)數(shù)相加得到哈希值,這樣算出來的哈希值會比較小,基本不會溢出,但是可能會造成哈希沖突。
??存在哈希沖突的情況下,我們對比子串的哈希值和模式串的哈希值,如果不相等還是證明兩個(gè)串不匹配;如果相等,由于存在哈希沖突的情況,我們還不能判定子串和模式串相等,還需要逐個(gè)字符進(jìn)行比較,如果每個(gè)字符都相同,才能證明匹配。
??在極端的情況下,存在大量哈希沖突,RK算法會退化BF算法,時(shí)間復(fù)雜度為O(n*m)。
KMP算法
KMP算法是一種改進(jìn)的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,也是由這三位作者的名字進(jìn)行命名的,用i 遍歷主串,j 遍歷模式串,主要的思想就是主串的 i 不回溯,模式串的 j 回溯到某個(gè)位置k,使得模式串的[0,k-1]的位置和主串[i-k,i-1]的位置直接匹配。來看看下面的例子:
??這個(gè)例子中,我們一直比較s[i]和pattern[j]的值,當(dāng)i=j=4的時(shí)候也就是上圖中的紅色框框的時(shí)候,s[i]!=pattern[j],此時(shí)如果按照BF算法,i 回溯到1,j 回溯到0繼續(xù)比對,但其實(shí)是不必要的,因?yàn)閕 回溯到1,s[i]=‘b’,而j 回溯到0,pattern[j]=‘a(chǎn)’,很明顯是不匹配的;來看看KMP算法的做法:主串不回溯也就是 i 不動,j 回溯到 k的位置,使得模式串的[0,k-1]的位置和主串[i-k,i-1]的位置直接匹配(下圖中 j 前的a和 i 前的a匹配,即藍(lán)色框框的部分直接匹配):
那現(xiàn)在問題就變成了位置k的確定。前面說到k的位置要滿足:模式串的[0,k-1]的位置和主串[i-k,i-1]的位置直接匹配,也就是第二幅圖中藍(lán)色框框的部分匹配。那再來看看第一幅圖中的兩個(gè)綠色框框,可以發(fā)現(xiàn)模式串的[j-k,j-1]和主串的[i-k,i-1]部分相等,整理得到:
pattern[0]~pattern[k-1] ?= ?s[i-k]~s[i-1] pattern[j-k]~pattern[j-1] ?= ?s[i-k]~s[i-1]兩個(gè)等式右邊部分都相等,所以我們可以得到兩個(gè)等式左邊部分相等:
pattern[0]~pattern[k-1] =pattern[j-k]~pattern[j-1]其實(shí)就是上面這幅圖中3個(gè)綠色的框框,整個(gè)道理也很簡單:就是因?yàn)閕 和 j前的a相等,而j 前的a 和 k前的a 相等,所以我們下次回溯的時(shí)候,就用k 前的a 對準(zhǔn) i 前的a,有機(jī)會可以匹配成功,而省略了BF算法中的多次不必要的回溯和比較。
??我們現(xiàn)在可以知道k的位置是由模式串本身來確定的,和主串無關(guān):pattern[0]~pattern[k-1] =pattern[j-k]~pattern[j-1],也就是說模式串中每一個(gè)字符pattern[j]都對應(yīng)著一個(gè)k值,這個(gè)k值只與模式串有關(guān),我們用next[j]表示pattern[j]對應(yīng)的k值,next數(shù)組的求解就是KMP算法的關(guān)鍵。
??next[j]就是模式串[0,j-1]的子串中真前綴和真后綴的最長可匹配長度,下面舉一個(gè)例子(模式串為ababc):
??j=0時(shí),next[0]=-1表示不存在,因?yàn)楫?dāng)j=0的時(shí)候,j-1=-1是非法下標(biāo)。
??j=1時(shí),[0,j-1]的子串只有一個(gè)a,沒有真前綴和真后綴(因?yàn)檎媲熬Y真后綴不能包含自身),此時(shí)next[1]=0。
??j=2時(shí),[0,j-1]的子串為ab,此時(shí)【真前綴為a,真后綴為b】,可最長匹配長度為0,所以next[2]=0;
??j=3時(shí),[0,j-1]的子串為aba,此時(shí)【真前綴為a,真后綴為a】,還有一種組合【真前綴為ab,真后綴為ba】,第一個(gè)組合的匹配長度為1,第二個(gè)組合的匹配長度為0,最長匹配長度為1,所以next[3]=1。
??j=4時(shí),[0,j-1]的子串為abab,此時(shí)的組合有:【真前綴為a,真后綴為b】,【真前綴為ab,真后綴為ab】,【真前綴為aba,真后綴為bab】,匹配長度分別為0,2,0,最長匹配長度為2,所以next[4]=2。
??上面這種方法只是我們用人腦計(jì)算真前綴和真后綴的最長可匹配長度,那要是讓計(jì)算機(jī)來如何計(jì)算呢?我們貼出代碼,再結(jié)合代碼進(jìn)行解釋:
void getnext(std::string &pattern, int next[]) {?? ?//next的長度為mint m = pattern.length();int k = -1 ,j = 0 ;next[0] = -1;while (j < m) {if (k == -1 || pattern[j] == pattern[k])next[++j] = ++k;elsek = next[k];} }
前三行代碼應(yīng)該沒啥問題,k和j相當(dāng)于兩個(gè)快慢指針,k用來找真前綴,j用來找真后綴。next[j]=k要滿足的關(guān)系也正如我們前面提到的:
接下來是while循環(huán),j的另一個(gè)作用是用來遍歷模式串的,所以循環(huán)的條件是j<m。
??先讓我們看下if分支,先忽略k==-1這個(gè)判定條件,pattern[j] == pattern[k]的時(shí)候next[++j] = ++k。這個(gè)應(yīng)該比較好理解,其實(shí)就和上面這條等式差不多,上面這條等式是next[j]=k要滿足的要求,而代碼這里是next[j+1]=k+1要滿足的要求。
??再來看看為什么要判斷k==-1,首先k初始化為-1,因?yàn)閖是從0開始的,j是快指針,k是慢指針,所以k要從-1開始。if分支里要訪問pattern[k]的值,k為-1的時(shí)候是非法下標(biāo);其次在else語句中,k = next[k]導(dǎo)致了k是會往前走的(也就是k會變小),有可能k回退到next[0]的時(shí)候又會變?yōu)?1了,所以在訪問pattern[k]的時(shí)候又會是非法下標(biāo)。綜上所述,判斷k==-1是為了保證訪問pattern[k]時(shí)數(shù)組下標(biāo)合法。
??整段代碼的關(guān)鍵就在于else分支的k = next[k](也就是k!=-1且pattern[j] != pattern[k]),也是最難懂的地方,讓我們來看個(gè)例子:
??如圖,現(xiàn)在已知next[13]=6,求next[14],所以兩個(gè)橙色框框內(nèi)長度為6的真前綴和真后綴是相同的(也就是0和7相同,1和8相同,2和9相同以此類推…最后5和12相同);假如現(xiàn)在走if分支:pattern[k]==pattern[j]即pattern[6]==pattern[13]的話,那么執(zhí)行next[++j] = ++k即next[14]=7,這個(gè)應(yīng)該沒啥問題。
??那如果走的else分支呢?pattern[k]!=pattern[j]即pattern[6]!=pattern[13]:k = next[k]
??現(xiàn)在假設(shè)next[k]=i,如圖所示,那么代表(0和1的綠框的兩個(gè)真前綴和4和5的藍(lán)框的后前綴相同),但是前面說過兩個(gè)橙色框相同,可以推出兩個(gè)綠框相同,兩個(gè)籃筐也相同,最終推導(dǎo)出兩個(gè)籃筐和兩個(gè)綠框總共4個(gè)框框里面的字符串都相同(即0、4,、7、11相同;1、5、8、12相同);我們的目的主要是為了證明(0和1的綠框 與 11和12的藍(lán)框相同),那么現(xiàn)在只需比較pattern[j]是否等于pattern[i],如果相等next[++j]=++i也即next[14]=3。而我們?yōu)榱诉壿嫿y(tǒng)一,將 i 賦值給k,即k=next[k],那么又可以回到if的分支和else的分支進(jìn)行判斷了。
?
??如果還不明白為什么k要初始化為1,代碼也可以這樣寫:k初始化為0,j初始化為1,如果pattern[j] == pattern[k]那么next[2]=1這里應(yīng)該沒問題,因?yàn)閗指向真前綴,j指向真后綴。但是仍要判斷k==-1,因?yàn)閚ext[0]=-1,k回退時(shí)有可能還是會出現(xiàn)-1的情況。其次 next[1]=0這里可能會溢出,因?yàn)閙=1的時(shí)候next數(shù)組的長度也只有1。我們可以將next[1]也并入下面的邏輯,也就是k從-1開始,j從0開始,同樣的if else邏輯也可以計(jì)算出next[1]=0,所以我們不采取下面這種寫法,而采用上面的寫法。
void getnext(std::string &pattern, int next[]) {?? ?//next的長度為mint m = pattern.length();int j = 1, k = 0;next[0] = -1;next[1] = 0;?? ?//模式串的長度m可能只有1,所以這里有可能會溢出while (j < m) {if (k == -1 || pattern[j] == pattern[k])?? ?//next[0]=-1,k回退時(shí)有可能還是會出現(xiàn)-1的情況next[++j] = ++k;elsek = next[k];} }??在構(gòu)建完next數(shù)組后,剩下的部分就簡單了:
int KMP(std::string& s, std::string& pattern) {int n = s.length(), m = pattern.length(), j = 0, i = 0;int* next = new int[m];getnext(pattern, next);while (i < n && j < m) {if (j == -1 || s[i] == pattern[j]) {++i;++j;}elsej = next[j];}if (j == m)?? ?//j走完整個(gè)pattern證明匹配成功,也會退出while循環(huán)return i - m; //返回匹配開始的位置,這里return i-j和return i-m是一樣的,因?yàn)閖==mreturn -1; }
??前面說過,kmp算法主串不回溯,也就是i是不回溯的,一直往上加遍歷主串,如果j==-1或者s[i] == pattern[j]時(shí),i和j分別自加,準(zhǔn)備下一個(gè)字符的比較,否則j回溯到next[j]的位置,回溯后pattern[0]~pattern[j-1] = s[i-j]~s[i-1],也就是說j前面的字符串和主串是已經(jīng)匹配的了,只需要繼續(xù)比較pattern[j]和s[i]。
??while循環(huán)結(jié)束的情況有:i==n或者j==m或者(i==n且j==m),只要j==m就證明匹配成功,因?yàn)閖走完了整個(gè)pattern,說明每個(gè)pattern的字符在主串中都能匹配上。完整代碼:
??KMP算法使用了額外的next數(shù)組,長度是pattern的長度m,所以空間復(fù)雜度是O(m)。
??getnext函數(shù)的時(shí)間復(fù)雜度為O(m),KMP函數(shù)中的while循環(huán)時(shí)間復(fù)雜度為O(n),因此KMP算法的時(shí)間復(fù)雜度為O(m+n)。
BM算法
我們在文本編輯器中,我們經(jīng)常用到查找及替換功能。比如說,在Word文件中,通過查找及替換功能,可以把某一個(gè)單詞統(tǒng)一替換成另一個(gè)單詞。對于文本編輯器這種軟件來說,查找及替換是其核心功能,我們希望使用的字符串匹配算法盡可能地高效。之前討論過RK算法,時(shí)間復(fù)雜度為O(n),其實(shí)已經(jīng)很高效了,現(xiàn)在來介紹一個(gè)新的字符串匹配算法,BM(Boyer-Moore)算法。
模式串和主串的匹配過程可以看成模式串在主串中不停的向后滑動。當(dāng)遇到不匹配的字符時(shí),BF算法和RK算法是將模式串往后滑動一位,然后從模式串的第一個(gè)字符開始重新匹配。
主串中的字符c在模式串中沒有對應(yīng)的字符,就肯定無法匹配。因此,我們可以一次性把模式串往后多滑動幾位,把模式串移動到字符c的后面,如下圖。
其實(shí),BM算法本質(zhì)上就是在尋找某種規(guī)律,借助這種規(guī)律,在模式串與主串匹配的時(shí)候,當(dāng)模式串與主串中的某個(gè)字符不匹配時(shí),能夠跳過一些肯定不會匹配的情況,將模式串往后多滑動幾位。字符串匹配的效率因此就高了。
4.2 BM算法的原理分析
BM算法的具體實(shí)現(xiàn)原理包含兩個(gè)部分:壞字符規(guī)則(bad character rule)和好后綴規(guī)則(good suffix rule)。
1.壞字符規(guī)則
我們知道在BF算法中的模式串和主串之間的匹配是按照下標(biāo)從小到大的順序進(jìn)行的,而BM算法的匹配順序卻是比較特殊,他是按照模式串下標(biāo)從大到小的順序倒敘進(jìn)行的。如下圖。
從模式串的末尾往前倒著匹配,當(dāng)發(fā)現(xiàn)某個(gè)字符無法匹配的時(shí)候,我們就把這個(gè)無法匹配的字符稱為“壞”字符。注意,壞字符指的是主串中的字符,而不是模式串中的字符。如下圖。
我們用壞字符c在模式串中查找,發(fā)現(xiàn)模式串中并不存在這個(gè)字符,也就是說,字符c與模式串中的任何字符都不可能匹配。這個(gè)時(shí)候,我們就可以將模式串直接滑動到字符c的后面,再重新從模式串的末尾字符開始比較。
?
我們將模式串滑動到c之后,就會發(fā)現(xiàn),模式串中的最后一個(gè)字符d,還是無法與主串中的字符a相互匹配。此時(shí),我們是否能將模式串滑動到主串中壞字符a(主串中第三個(gè)a)的后面?
其實(shí)是不可以的,因?yàn)閴淖址鸻在模式串中存在,也就是下標(biāo)為0的位置存儲的就是字符a。因此,我們可以將模式串往后滑動兩位,讓模式串中的a與主串中的第三個(gè)a上下對齊,然后再從模式串的末尾字符開始匹配。
?
?現(xiàn)在大家可能會發(fā)現(xiàn)一個(gè)問題,那就是,第一次我們移動模式串的時(shí)候,是移動了三位,但是第二次的時(shí)候就是移動了兩位,那么對于移動的具體次數(shù),有沒有規(guī)律呢?
當(dāng)模式串與主串不匹配時(shí),我們把壞字符對應(yīng)的模式串中的字符在模式串中的下標(biāo)記作為si。如果壞字符在模式串中存在,那么我們把壞字符在模式串中的下標(biāo)記作xi,如果壞字符在模式串中不存在,那么我們把xi記作-1。那么,模式串往后滑動的位數(shù)就等于si-xi。
?
?這里還要說明的是,如果壞字符在模式串中出現(xiàn)多次,那么在計(jì)算xi的時(shí)候,我們選擇模式串中最靠后的哪個(gè)壞字符的下標(biāo)作為xi的值。這樣就不會因?yàn)槟J礁Z滑動過多,而導(dǎo)致本來可能匹配的情況被忽略。
利用壞字符規(guī)則,BM算法在最好情況下的時(shí)間復(fù)雜度非常底,是O(n/m)。例如,主串是aaabaaabaaabaaab,模式串是aaaa,每當(dāng)模式串與主串不匹配時(shí)(壞字符是b),我們就可以將模式串直接往后滑動4位,因此,匹配具有類似特點(diǎn)的主串與模式串的時(shí)候,BM算法是高效的。
不過,單純使用壞字符規(guī)則還不夠,因?yàn)楦鶕?jù)si-xi計(jì)算出來的滑動位數(shù)又可能是負(fù)數(shù),如主串是aaaaaaaaaaaa,模式串為baaa。針對這種情況,就還需要另外一種規(guī)則,好后綴規(guī)則。
2.好后綴規(guī)則
好后綴規(guī)則與壞字符規(guī)則非常類似,當(dāng)模式串滑動到如下圖所示的位置時(shí),模式串與主串有兩個(gè)字符是匹配的,倒數(shù)第三個(gè)字符不匹配。
?
先來看看好后綴規(guī)則怎么工作的。
我們把已經(jīng)匹配的"bc"稱為好后綴,記作{u}.我們用他在模式串中進(jìn)行查找,如果找到另一個(gè)與好后綴{u}匹配的子串{u*},那么我們就將模式串滑動到子串{u*}與好后綴{u}上下對齊的位置。如下圖。
?
?如果在模式串中找不到好后綴{u}匹配的另外的子串,就直接將模式串滑動到好后綴{u}的后面。
?
?不過大家想沒想過這個(gè)問題,當(dāng)模式串中不存在與好后綴{u}匹配的子串時(shí),我們直接將模式串滑動到好后綴{u}后面,是不是有點(diǎn)過頭,看下圖一個(gè)例子,其中"bc"是好后綴,盡管在模式串中沒有另外一個(gè)與好后綴匹配的子串,但是我們?nèi)绻J酱苿拥胶煤缶Y的后面,就會錯(cuò)過模式串和主串可以匹配的情況。
?
如果好后綴在模式串中不存在可匹配的子串,那在我們一步一步往后滑動模式串的過程中, 只要主串中的{u}與模式串有重合,那肯定就無法完全匹配。但是當(dāng)模式串滑動到前綴與主 串中{u}的后綴有部分重合的時(shí)候,并且重合的部分相等的時(shí)候,就有可能會存在完全匹配 的情況。
?
?
所以,針對這種情況,我們不僅要看好后綴在模式串中,是否有另一個(gè)匹配的子串,我們還
要考察好后綴的后綴子串,是否存在跟模式串的前綴子串匹配的。
所謂某個(gè)字符串 s 的后綴子串,就是最后一個(gè)字符跟 s 對齊的子串,比如 abc 的后綴子串 就包括 c, bc。所謂前綴子串,就是起始字符跟 s 對齊的子串,比如 abc 的前綴子串有 a, ab。我們從好后綴的后綴子串中,找一個(gè)最長的并且能跟模式串的前綴子串匹配的,假設(shè) BM是{v},然后將模式串滑動到如圖所示的位置。
?
當(dāng)模式串和主串中的 某個(gè)字符不匹配的時(shí)候,如何選擇用好后綴規(guī)則還是壞字符規(guī)則,來計(jì)算模式串往后滑動的 位數(shù)?
我們可以分別計(jì)算好后綴和壞字符往后滑動的位數(shù),然后取兩個(gè)數(shù)中最大的,作為模式串往 后滑動的位數(shù)。這種處理方法還可以避免我們前面提到的,根據(jù)壞字符規(guī)則,計(jì)算得到的往 后滑動的位數(shù),有可能是負(fù)數(shù)的情況。
BM算法的代碼實(shí)現(xiàn)
“壞字符規(guī)則”本身不難理解。當(dāng)遇到壞字符時(shí),要計(jì)算往后移動的位數(shù) si-xi,其中 xi 的 計(jì)算是重點(diǎn),我們?nèi)绾吻蟮?xi 呢?或者說,如何查找壞字符在模式串中出現(xiàn)的位置呢?如果我們拿壞符在模式串中順序遍歷查找,這樣就會比較低效,勢必影響這個(gè)算法的性 能。有沒有更加高效的方式呢?我們之前學(xué)的散列表,這里可以派上用場了。我們可以將模 式串中的每個(gè)字符及其下標(biāo)都存到散列表中。這樣就可以快速找到壞字符在模式串的位置下 標(biāo)了。
關(guān)于這個(gè)散列表,我們只實(shí)現(xiàn)一種最簡單的情況,假設(shè)字符串的字符集不是很大,每個(gè)字符 長度是 1 字節(jié),我們用大小為 256 的數(shù)組,來記錄每個(gè)字符在模式串中出現(xiàn)的位置。數(shù)組 的下標(biāo)對應(yīng)字符的 ASCII 碼值,數(shù)組中存儲這個(gè)字符在模式串中出現(xiàn)的位置。
?
哈希表的代碼構(gòu)建過程
private static final int SIZE = 256;//全局變量或成員變量//b為模式串,m為模式串長度,bc為哈希表 pivate void generateBC(char[] b,int m,int[] bc){for(int i = 0;i < SIZE;++i){bc[i] = -1; ? //初始化bc}for(int i = 0;i < m;++i){int ascii = (int)b[i];//計(jì)算b[i]的ASCII值bc[ascii] = i;?}? }
掌握了壞字符規(guī)則之后,我們先把 BM 算法代碼的大框架寫好,先不考慮好后綴規(guī)則,僅
用壞字符規(guī)則,并且不考慮 si-xi 計(jì)算得到的移動位數(shù)可能會出現(xiàn)負(fù)數(shù)的情況。
?BM算法代碼實(shí)現(xiàn)中的關(guān)鍵變量。
?
大家載堅(jiān)持一下看完好后綴規(guī)則的實(shí)現(xiàn)
我們先簡單回顧一下,前面講過好后綴的處理規(guī)則中最核心的內(nèi)容:
在模式串中,查找跟好后綴匹配的另一個(gè)子串;
在好后綴的后綴子串中,查找最長的、能跟模式串前綴子串匹配的后綴子串;
在不考慮效率的情況下,這兩個(gè)操作都可以用很“暴力”的匹配查找方式解決。但是,如果 想要 BM 算法的效率很高,這部分就不能太低效。如何來做呢?
因?yàn)楹煤缶Y也是模式串本身的后綴子串,所以,我們可以在模式串和主串正式匹配之前,通 過預(yù)處理模式串,預(yù)先計(jì)算好模式串的每個(gè)后綴子串,對應(yīng)的另一個(gè)可匹配子串的位置。這 個(gè)預(yù)處理過程比較有技巧,很不好懂,應(yīng)該是這節(jié)最難懂的內(nèi)容了,你要認(rèn)真多讀幾遍。 我們先來看,如何表示模式串中不同的后綴子串呢? 因?yàn)楹缶Y子串的最后一個(gè)字符的位置是 固定的,下標(biāo)為 m-1,我們只需要記錄長度就可以了。通過長度,我們可以確定一個(gè)唯一 的后綴子串
?
現(xiàn)在,我們要 引入最關(guān)鍵的變量 suffix 數(shù)組 。suffix 數(shù)組的下標(biāo) k,表示后綴子串的長 度,下標(biāo)對應(yīng)的數(shù)組值存儲的是,在模式串中跟好后綴{u}相匹配的子串{u*}的起始下標(biāo)值。這句話不好理解,我舉一個(gè)例子。
?
但是,如果模式串中有多個(gè)(大于 1 個(gè))子串跟后綴子串{u}匹配,那 suffix 數(shù)組中該存儲 哪一個(gè)子串的起始位置呢?為了避免模式串往后滑動得過頭了,我們肯定要存儲模式串中最 靠后的那個(gè)子串的起始位置,也就是下標(biāo)最大的那個(gè)子串的起始位置。不過,這樣處理就足 夠了嗎?
實(shí)際上,僅僅是選最靠后的子串片段來存儲是不夠的。我們再回憶一下好后綴規(guī)則。 我們不僅要在模式串中,查找跟好后綴匹配的另一個(gè)子串,還要在好后綴的后綴子串中,查 找最長的能跟模式串前綴子串匹配的后綴子串。
如果我們只記錄剛剛定義的 suffix,實(shí)際上,只能處理規(guī)則的前半部分,也就是,在模式串 中,查找跟好后綴匹配的另一個(gè)子串。所以,除了 suffix 數(shù)組之外,我們還需要另外一個(gè) boolean 類型的 prefix 數(shù)組,來記錄模式串的后綴子串是否能匹配模式串的前綴子串。
?
?
現(xiàn)在,我們來看下, 如何來計(jì)算并填充這兩個(gè)數(shù)組的值 ?這個(gè)計(jì)算過程非常巧妙。
我們拿下標(biāo)從 0 到 i 的子串(i 可以是 0 到 m-2)與整個(gè)模式串,求公共后綴子串。如果
公共后綴子串的長度是 k,那我們就記錄 suffix[k]=j(j 表示公共后綴子串的起始下標(biāo))。
如果 j 等于 0,也就是說,公共后綴子串也是模式串的前綴子串,我們就記錄
prefix[k]=true。
?
我們把 suffix 數(shù)組和 prefix 數(shù)組的計(jì)算過程,用代碼實(shí)現(xiàn)出來,就是下面這個(gè)樣子:?
void generateGS(char[] b,int m,int[] suffix,boolean[] prefix){for(int i = 0;i < m;++i){//初始化suffix,prefix數(shù)組suffix[i] = -1;prefix[i] = false;}for(int i = 0;i < m - 1;++i){//循環(huán)處理b[0,i]int j = i;intk = 0;//公共后綴子串的長度while(j >= 0 && b[j] == b[m-1-k]){//與b[0,m-1]求公共后綴子串--j;++k;suffix[k] = j+1;//j+1表示公共后綴子串在b[0,i]中的起始下標(biāo)}if(j == -1) prefix[k] = true; ? //公共后綴子串也是模式串的前綴子串} }有了這兩個(gè)數(shù)組之后,我們現(xiàn)在來看, 在模式串跟主串匹配的過程中,遇到不能匹配的字符
時(shí),如何根據(jù)好后綴規(guī)則,計(jì)算模式串往后滑動的位數(shù)?
假設(shè)好后綴的長度是 k。我們先拿好后綴,在 suffix 數(shù)組中查找其匹配的子串。如果
suffix[k] 不等于 -1(-1 表示不存在匹配的子串),那我們就將模式串往后移動 j-suffix[k]+1 位(j 表示壞字符對應(yīng)的模式串中的字符下標(biāo))。如果 suffix[k] 等于 -1,表示 模式串中不存在另一個(gè)跟好后綴匹配的子串片段。我們可以用下面這條規(guī)則來處理。
?
好后綴的后綴子串 b[r, m-1](其中,r 取值從 j+2 到 m-1)的長度 k=m-r,如果 prefix[k] 等于 true,表示長度為 k 的后綴子串,有可匹配的前綴子串,這樣我們可以把模 式串后移 r 位。
?
如果兩條規(guī)則都沒有找到可以匹配好后綴及其后綴子串的子串,我們就將整個(gè)模式串后移
m 位。
?
至此,好后綴規(guī)則的代碼實(shí)現(xiàn)我們也講完了。我們把好后綴規(guī)則加到前面的代碼框架里,就
可以得到 BM 算法的完整版代碼實(shí)現(xiàn)。
BM 算法的性能分析及優(yōu)化
我們先來分析 BM 算法的內(nèi)存消耗。
整個(gè)算法用到了額外的 3 個(gè)數(shù)組,其中 bc 數(shù)組的大 小跟字符集大小有關(guān),suffix 數(shù)組和 prefix 數(shù)組的大小跟模式串長度 m 有關(guān)。 如果我們處理字符集很大的字符串匹配問題,bc 數(shù)組對內(nèi)存的消耗就會比較多。因?yàn)楹煤?綴和壞字符規(guī)則是獨(dú)立的,如果我們運(yùn)行的環(huán)境對內(nèi)存要求苛刻,可以只使用好后綴規(guī)則,不使用壞字符規(guī)則,這樣就可以避免 bc 數(shù)組過多的內(nèi)存消耗。不過,單純使用好后綴規(guī)則 的 BM 算法效率就會下降一些了。
對于執(zhí)行效率來說,我們可以先從時(shí)間復(fù)雜度的角度來分析。
基于我目前討論的這個(gè)BM算法,在極端情況下,預(yù)處理計(jì)算 suffix 數(shù)組、prefix 數(shù)組 的性能會比較差。
比如模式串是 aaaaaaa 這種包含很多重復(fù)的字符的模式串,預(yù)處理的時(shí)間復(fù)雜度就是 O(m^2)。當(dāng)然,大部分情況下,時(shí)間復(fù)雜度不會這么差。
總結(jié)
- 上一篇: dnastar拼接反向互补序列_DNAs
- 下一篇: java零基础到精通全套视频教程