字符串匹配算法(二):BM(BoyerMoore)算法、坏字符规则,好后缀规则
文章目錄
- BM算法
- 壞字符規(guī)則
- 好后綴規(guī)則
- 完整代碼
BM算法
BM算法的全程叫做Boyer-Moore,是工程上最常用且最高效的字符串匹配算法,有實(shí)驗(yàn)統(tǒng)計(jì),它的性能是著名的KMP 算法的 3 到 4 倍。那么它是如何將性能提升的呢?
在上一篇博客中我介紹了BF算法和RK算法,其中也提到過如果想要優(yōu)化字符串匹配的效率,就必須要減少不必要的比較,例如RK算法就是通過預(yù)匹配哈希值來(lái)完成了這一功能,但是我們也提到了,由于哈希沖突等原因,RK在最壞的情況下就會(huì)退化成BF算法。
基于上述問題的缺陷,BM、KMP(下一篇博客會(huì)寫)等算法采用了大量滑動(dòng)的機(jī)制來(lái)解決這一問題
在RK和BF算法中,在字符串不匹配的時(shí)候,我們通常會(huì)將模式串滑動(dòng)到主串的下一個(gè)位置繼續(xù)進(jìn)行匹配,這種方法存在一定的缺陷,就是即使我們滑動(dòng)到的位置不可能完成匹配,我們還是會(huì)一個(gè)一個(gè)去嘗試進(jìn)行配對(duì),這也就是它們效率低下的原因。
就例如上圖,我們可以發(fā)現(xiàn)a只存在于主串的第一個(gè)位置,第四個(gè)位置,第六個(gè)位置,而其他的位置下模式串是不可能匹配成功的,所以我們滑動(dòng)的時(shí)候就應(yīng)該直接滑動(dòng)到上述的位置,如下圖
BM算法的核心就是找到這種大量滑動(dòng)的規(guī)律,減少無(wú)意義的匹配。而它正是通過壞字符規(guī)則與好后綴規(guī)則來(lái)實(shí)現(xiàn)。
壞字符規(guī)則
因?yàn)槲覀兊膲淖址秃煤缶Y規(guī)則都需要保證偏移量最大,所以其并非像傳統(tǒng)的字符串比較一樣從前往后,而是從后往前比較,并且我們將第一個(gè)遇見的不匹配的字符稱為壞字符
當(dāng)檢測(cè)到壞字符后,我們就沒有必要再一個(gè)一個(gè)的進(jìn)行判斷了,因?yàn)?font color="red">只有模式串與壞字符T對(duì)齊的位置也是字符T的情況下,兩者才有匹配的可能。并且為了保證滑動(dòng)的范圍最大,我們對(duì)字符T的選擇是在模式串中最后一次出現(xiàn)的那個(gè)
壞字符規(guī)則主要有以下三種情況,下面一一對(duì)其進(jìn)行分析
情況一:模式串中存在與壞字符相同的字符
此時(shí)的處理方法就是將壞字符與匹配字符對(duì)其,接著進(jìn)行判斷
此時(shí)匹配成功
情況二:模式串中不存在與壞字符相同的字符
此時(shí)模式串中不存在可以與壞字符匹配的字符,這也就代表著在壞字符這個(gè)位置之前,不可能匹配成功,所以我們直接滑動(dòng)到壞字符的下一個(gè)位置
此時(shí),匹配成功。
情況三:倒退或者不移動(dòng)
例如以下情景
后面的全部匹配,不匹配的只有b,而壞字符又在最后面出現(xiàn)過,此時(shí)就會(huì)倒退。
所以我們還需要加上判斷,如果滑動(dòng)值小于等于0時(shí),就直接向后滑動(dòng)一步。
為了保存每個(gè)字符的最后一次出現(xiàn)的下標(biāo),我們使用一個(gè)數(shù)組來(lái)模擬哈希,采用ascii碼來(lái)進(jìn)行直接定址
下面是基于壞字符規(guī)則實(shí)現(xiàn)的BM算法
//構(gòu)建壞字符規(guī)則的下標(biāo)數(shù)組 void generateBC(const string& pattern, int* indexArr, int len) {//初始化for(int i = 0; i < len; i++){indexArr[i] = -1;}//記錄模式串中每個(gè)下標(biāo)最后出現(xiàn)的位置for(int i = 0; i < pattern.size(); i++){indexArr[pattern[i]] = i; } }int boyerMoore(const string& str, const string& pattern) {//不滿足條件則直接返回falseif(str.empty() || pattern.empty() || str.size() < pattern.size()){return -1;}int len1 = str.size(), len2 = pattern.size();int indexArr[128] = {0}; //壞字符規(guī)則記錄數(shù)組,記錄了每一個(gè)字符最后一次出現(xiàn)的下標(biāo)generateBC(pattern, indexArr, 128);int i = 0;while(len1 - i >= len2){int j;//模式串從后往前匹配for(j = len2 - 1; j >= 0; j--){//如果當(dāng)前字符不匹配,則說明該位置是壞字符if(str[i + j] != pattern[j]){break;}}//如果全部匹配,則返回主串起始位置if(j < 0){return i;}/*如果該字符沒出現(xiàn)過,則直接將模式串滑動(dòng)到壞字符的下一個(gè)位置如果出現(xiàn)過,則將模式串中對(duì)應(yīng)字符滑動(dòng)到壞字符處*/int badMove = (j - indexArr[str[i + j]]);badMove = (badMove == 0) ? 1 : badMove; //防止倒退i += badMove;}return -1; }從上面也可以看出,在最后一種情況下BM算法的效率就又會(huì)退化到BF算法的級(jí)別,所以為了防止這種問題,BM還有一種好前綴規(guī)則
好后綴規(guī)則
在我們進(jìn)行匹配的時(shí)候,我們將第一次碰到的不匹配的字符稱為壞字符,而將碰到壞字符之前所匹配到的字符串稱為好后綴
與壞字符規(guī)則一樣,如果我們想要使得字符串匹配,只有模式串中存在相同子串,并與主串中好后綴對(duì)齊的情況下,兩者才有匹配的可能。所以直接將對(duì)應(yīng)子串滑動(dòng)到好后綴的位置,如下圖
如果不存在這個(gè)子串,那我們能否按照壞字符規(guī)則,則直接跳過好后綴后呢?
答案是否定的,如果我們因?yàn)檫^度滑動(dòng)導(dǎo)致我們跳過了本身可匹配的一些字符串,如下圖
這是為什么呢?雖然我們的模式串中并不存在能夠與好后綴匹配的子串,但是卻存在能夠與好后綴部分重合的子串,而我們的滑動(dòng)就導(dǎo)致了跳過了這些子串
為了防止上述情況,我們此時(shí)就會(huì)尋找能夠與部分好后綴子串匹配的前綴,并以它為滑動(dòng)的標(biāo)準(zhǔn),如下圖
為了方便計(jì)算,我們需要保存模式串中所有前綴和后綴的匹配情況以及子串的位置,所以需要引入兩個(gè)數(shù)組,一個(gè)是整型數(shù)組suffix,其用于標(biāo)記能夠與好后綴匹配的子串的下標(biāo)。另一個(gè)是布爾數(shù)組prefix,其用于標(biāo)記前綴[0, i - 1]是否能夠與好后綴進(jìn)行匹配。
完整代碼
好前綴和壞字符都實(shí)現(xiàn)了,因?yàn)槲覀兿M氖潜M量減少不必要的匹配,所以我們選取兩者中較大的那一個(gè)作為偏移量。
將上面的好前綴規(guī)則加入前面寫的壞字符規(guī)則的框架中,就是完整的BM算法,代碼如下
//構(gòu)建壞字符規(guī)則的下標(biāo)數(shù)組 void generateBC(const string& pattern, vector<int>& indexArr) {//記錄模式串中每個(gè)下標(biāo)最后出現(xiàn)的位置for(int i = 0; i < pattern.size(); i++){indexArr[pattern[i]] = i; } }//構(gòu)建好后綴規(guī)則的前綴和后綴數(shù)組 void generateGS(const string& pattern, vector<int>& suffix, vector<bool>& prefix) {int len = suffix.size();//匹配區(qū)間[0 ~ len - 1],len時(shí)即為整個(gè)模式串,不可能存在前綴for(int i = 0; i < len - 1; i++){int j = i;int size = 0;while(j >= 0 && pattern[j] == pattern[len - 1 - size]){//繼續(xù)匹配下一個(gè)位置j--; size++; suffix[size] = j + 1; //記錄匹配后綴的子串的位置}//如果子串一直匹配到開頭,則說明該子串為前綴,此時(shí)前綴與后綴匹配if(j == -1){prefix[size] = true;}} }int moveByGS(int index, const vector<int>& suffix, const vector<bool>& prefix) {int len = suffix.size(); //模式串長(zhǎng)度int size = len - 1 - index; //后綴長(zhǎng)度//如果存在與后綴匹配的子串,則直接返回它們的偏移量if(suffix[size] != -1){return index - suffix[size] + 1;}//如果沒有匹配的后綴,那么判斷后綴中是否有部分與前綴匹配for(int i = index + 2; i <= len - 1; i++){if(prefix[len - i] == true){return i;}}//如果也不存在,則說明沒有任何匹配,直接偏移整個(gè)模式串的長(zhǎng)度return len; }int boyerMoore(const string& str, const string& pattern) {//不滿足條件則直接返回falseif(str.empty() || pattern.empty() || str.size() < pattern.size()){return -1;}int len1 = str.size(), len2 = pattern.size();vector<int> indexArr(128, -1); //標(biāo)記匹配壞字符的字符下標(biāo)vector<int> suffix(len2, -1); //標(biāo)記匹配后綴的子串下標(biāo)vector<bool> prefix(len2, false); //標(biāo)記是否匹配前綴generateBC(pattern, indexArr);generateGS(pattern, suffix, prefix);int i = 0;while(len1 - i >= len2){int j;//模式串從后往前匹配for(j = len2 - 1; j >= 0; j--){//如果當(dāng)前字符不匹配if(str[i + j] != pattern[j]){break;}}//如果全部匹配,則返回主串起始位置if(j < 0){return i;}int badMove = (j - indexArr[str[i + j]]); //壞字符規(guī)則偏移量badMove = (badMove == 0) ? 1 : badMove; //防止倒退int goodMove = 0; //好后綴規(guī)則偏移量//如果一個(gè)都不匹配,則不存在后綴if(j < len2 - 1){goodMove = moveByGS(j, suffix, prefix); //計(jì)算出好后綴的偏移量}i += max(goodMove, badMove); //加上最大的那個(gè)}return -1; }總結(jié)
以上是生活随笔為你收集整理的字符串匹配算法(二):BM(BoyerMoore)算法、坏字符规则,好后缀规则的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 字符串匹配算法(一):BF(BruteF
- 下一篇: 字符串匹配算法(三):KMP(Knuth