字串查詢演算法總結及MS的strstr原始碼
http://www.cnblogs.com/ziwuge/archive/2011/12/09/2281455.html
首先來說說字串的查詢,即就是在一個指定的字串A中查詢一個指定字串B出現的位置或者統計其他B在A中出現的次數等等相關查詢。
①MS自己提供了一個strstr函式原型:extern char *strstr(char *str1, char *str2);標頭檔案<string.h>。但也可不包含標頭檔案直接使用下面程式碼:
char * __cdecl strstr ( const char * str1, const char * str2 ) { char *cp = (char *) str1; char *s1, *s2; if ( !*str2 ) return((char *)str1); while (*cp) { s1 = cp; s2 = (char *) str2; while ( *s1 && *s2 && !(*s1-*s2) ) s1++, s2++; if (!*s2) return(cp); cp++; } return(NULL); }
可直接將char 替換為 wchar_t用於寬字元。不過在字串的查詢中效率是最低。
②KMP演算法 詳細介紹可參考下面:1、KMP演算法 2、KMP演算法詳解
其實KMP演算法可對裡面的陣列然後會更優化一些。
③Sunday 是BM查詢演算法的變種,但是效率更好。下面摘自部落格園園友Aga.J的文章:
Sunday演算法是一種比KMP和BM更加高效的匹配演算法,它的思想跟BM演算法相似,在匹配進行時,Sunday演算法失敗時關注的是源字串中當前參加匹配的長度為M(模式串的長度)的最後一位字元的下一個字元。如果該字元不在模式串中出現,那麼就將直接跳過,即移動步長=模式串的長度+1,否則,則移動步長=模式串中最右端的該字元到末尾的距離加1。
假設我們要匹配”HEREISASIMPLEEXAMPLE”和”EXAMPLE”
Text:HERE IS A SIMPLE EXAMPLE(此處中間是一個空格,用兩個空格是為了對齊.下同)
Pattern:EXAMPLE
我們從源串的初始位置開始和模式串比較,發現第一個就不匹配了,這時候如果使用樸素演算法,那麼我們就只是將模式串右移一位,而使用KMP演算法的話,則根據自身的模式陣列來確定移動的步長,使用Sunday演算法時,我們會比較源串對齊後的下一個字元,也就是text中IS後面的空格,因為無論我們用什麼方法去移動模式串,這個字元總是要參與下一次匹配(假設我們只移動1位或者小於模式串長度位,這個空格所在的位一定要參與匹配,不然我們就可能遺漏掉可能的匹配,而假設在小於模式串長度的位的移動中沒有匹配,那麼下一個起始匹配點就是空格的所在位)。
既然知道該為一定要匹配,那麼就和模式串進行比較,如果模式串中不存在該字元(這裡是空格),那麼就直接跳到空格字元的下一個字元進行匹配(這時候從E和A開始匹配)
Text:HERE IS A SIMPLE EXAMPLE
Pattern:EXAMPLE
這時候再次進行匹配的判斷,發現第一個字元又不匹配,所以和上面的方法一樣,我們直接看對齊後的下一個字元E,這時候拿E從右往左和模式串比較,如果模式串中存在E,那麼就移動模式串知道兩個E對齊,這樣得到(這種從後往前,從右往左的匹配思想是從BM演算法借鑑過來的,從後往前比較的優點是一旦遇到不匹配的時候,可以跳躍的距離更大,因為後面都不匹配了,前面再匹配都沒有用了,所以這裡拿E從模式串中從右往左匹配,找到第一個和E匹配的位置—詳細看BM演算法的分析!)
Text:HERE IS A SIMPLE EXAMPLE
Pattern:EXAMPLE
接下來還是從模式串的首位和源串的新起始位開始比較,發現,又無法匹配,而再判斷對齊的後一位,這次還是將整個模式串移動到新的不匹配位空格之後,最後完成匹配。
上述例子是從網上摘錄下來的,不是很典型,因為在第二次匹配的時候,剛好源串對齊後的下一位就和模式串的最後一位匹配,所以體現不出Sunday演算法的模式串移動的過程。
/* * 演算法分析: * 1 從第一個字元開始,對pattern和source逐個字元比較 * 2 如果出現匹配失敗,則檢查source的在pattern末尾位置的後一個字元是否和pattenr的某個一個字元相等 * 如果是,則對其到那個相等字元(從右往左對齊),重新執行 * 如果不是,則將pattern和source的比較初始位設定在source的後後個字元,再執行 * 3 如果匹配,則成功 */ #include<iostream> using namespace std; // 呼叫前先檢測源串是否超過長度,函式返回 以源串的sourceStartPos為比較的起始點,第一個和模式串不匹配的字元的位置, int compare(char* source, char* pattern, int sourceStartPos, int patternLength) { int i = 0; for(; ((i < patternLength) && (source[sourceStartPos+i] == pattern[i])); i++) { ; } return i; // 返回pattern中無法匹配的元素的位置i,pattern[i] } bool sundayMatch(char* source, char* pattern, int sourceLength, int patternLength) { int startPos = 0; // 源串的起始比較點 int failMatchPos = 0; // 失效點 int j = 0; while( (startPos+patternLength -1) <= sourceLength) // 源串中剩下的子串的長度比模式串短,即還可以繼續比較 { failMatchPos=compare(source,pattern,startPos,patternLength); // 獲得首次失配的字元在源串中的位置 cout<<"failMatchPos:"<<failMatchPos<<" "; if( failMatchPos == patternLength) // 如果剛好和模式串一樣長,即匹配成功 return true; else { for( j = patternLength-1; j >= 0; j--) // 檢視源串當前匹配子串的下一個字元是否和模式串中的任意一個字元匹配,注意是從右往左查詢 if( source[startPos+patternLength] == pattern[j]) { startPos += patternLength - j; break; // 一旦存在,則初始化下一個起始匹配點,即上述所謂的對齊 } if(j < 0) // 如果不存在,則跳過 startPos = startPos+patternLength + 1; } cout<<"newStartPos"<<startPos<<endl; } return false; } void main() { char *s1 = "THIS IS A EXAMPLE"; // HERE_IS_A_SIMPLE_EXAMPLE 24 char *s2 = "EXAMPLE"; bool result = sundayMatch(s1, s2, 17, 7); if( result ) cout<<"yes"; int i = 0; cin>>i; }
網路上的實現方法是這樣的,先做一個預處理,針對子串中每個出現的字元,儲存源串對齊後的下一位一旦出現匹配或者不匹配所需要移動的距離,然後在匹配過程中就可以直接使用,不需要像我寫的方法那樣重複的判斷某個位的字元是否和模式串中出現及其出現位置是哪裡。(這種方法花費了空間但贏得了時間)
/* 採用BM/KMP的預處理的做法,事先計算好移動步長,等到遇到不匹配的值直接使用 */ #include <iostream> #include <string.h> using namespace std; #define MAX_CHAR_SIZE 256 //一個字元8位 最大256種 /* * 設定每個字元最右移動步長,儲存每個字元的移動步長 * 如果大串中匹配字元的右側一個字元沒在子串中,大串移動步長=整個串的距離+1 * 如果大串中匹配範圍內的右側一個字元在子串中,大串移動距離=子串長度-這個字元在子串中的位置 */ int *setCharStep(char *subStr) { int *charStep = new int[MAX_CHAR_SIZE]; // 程式碼沒有注意安全:) int subStrLen = strlen(subStr); for(int i = 0; i < MAX_CHAR_SIZE; i++) charStep[i] = subStrLen + 1; // 如果大串中匹配字元的右側一個字元沒在子串中,大串移動步長=整個串的距離+1 // 從左向右掃描一遍 儲存子串中每個字元所需移動步長 for(int j = 0; j < subStrLen; j++) { charStep[(unsigned char)subStr[i]] = subStrLen - i; // 如果大串中匹配範圍內的右側一個字元在子串中,大串移動距離=子串長度-這個字元在子串中的位置 } return charStep; } /* * 演算法核心思想,從左向右匹配,遇到不匹配的看大串中匹配範圍之外的右側第一個字元在小串中的最右位置 * 根據事先計算好的移動步長移動大串指標,直到匹配 */ int sundaySearch(char* mainStr,char* subStr,int* charStep) { int mainStrLen = strlen(mainStr); int subStrLen = strlen(subStr); int main_i = 0; int sub_j = 0; while (main_i < mainStrLen) { int tem = main_i; // 儲存大串每次開始匹配的起始位置,便於移動指標 while(sub_j < subStrLen) { if(mainStr[main_i] == subStr[sub_j]) { main_i++; sub_j++; continue; } else{ // 如果匹配範圍外已經找不到右側第一個字元,則匹配失敗 if( (tem + subStrLen) > mainStrLen) return -1; // 否則,移動步長,重新匹配 char firstRightChar = mainStr[tem+subStrLen]; main_i = tem+charStep[(unsigned char)firstRightChar]; sub_j = 0; break; // 退出本次失敗匹配 重新一輪匹配 } } if(sub_j == subStrLen) return (main_i - subStrLen); } return -1; } int main() { char* mainStr = "absaddsasfasdfasdf"; char* subStr = "dd"; int* charStep = setCharStep(subStr); cout<<"位置:"<<sundaySearch(mainStr,subStr,charStep)<<endl; system("pause"); return 0; }
【參考資料 感謝作者】
以上部分摘自: 字串匹配演算法之Sunday演算法的學習筆記
另外例舉幾篇關於字串查詢的文章:
1、KMP演算法
2、KMP 演算法並非字串查詢的優化 [轉]
3、精確字串匹配(BM演算法) [轉]
4、sunday演