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