Author: bakari Date: 2012/8/9
繼上篇。。。。。
下面是我寫的代碼與源碼作的一些比較,均已嚴格測試通過,分別以“string 之”系列述之。
strchr函數:求字符在字符串中所在的位置
strstr函數:求子串在主串中的起始位置(用的字符串的模式匹配算法)
1 char * Mystrchr(const char *str, char c); //c第一次出現的位置 2 //BF algorithm 3 int Mystrstr_BF(char *mainStr, char *subStr); //子串第一次出現的位置 4 //KMP algorithm 5 int Mystrstr_KMP(char *mainStr, char *subStr);
1 /******************************************************* 2 * strchr 3 *******************************************************/ 4 char * Mystrchr(const char *str, char c){ //c第一次出現的位置 5 assert(NULL != str); 6 for(; *str != c; str ++){ 7 if(*str == '\0') 8 return NULL; 9 } 10 return (char * )str; 11 }
下面着重講解BF算法和KMP算法,要真正懂一個算法並將它吃透,一定要懂這個算法的歷史,回到最初去了解這個算法是怎樣被發現的。對於相對感性的東西不用追本溯源,咬文嚼字,但是對於理性(換個詞叫抽象)的東西一定不能急躁,即使短時間內能夠清楚了解之,但過了一陣之后毫無疑問會忘記,本人上學期上DS的時候已經學過這個算法,當時就是為了坑爹的考試,沒有好好吃透,導致現在需要又要重新去復習回味。所以,為了做個高效的人士,還是那句老話,欲速則不達,好的算法就應該慢下心來慢慢品味,從它的根抓起。將之吃透!
本文是不會跟你去講歷史的,So,想知道歷史的就Google,百度吧,在下也幫不了,本文只做簡單的總結,加深印象。
一 、BF算法
這個算法符合人的思維過程,不用轉彎,一看便知.為了顯示清晰,用途代替文字
看着圖就可以寫代碼了:
1 int BF(const char * str1, const char * str2){ 2 assert(NULL != str1 && NULL != str2); 3 int i = 0, j = 0; 4 5 while(*(str1 + i) && *(str2 + j)){ 6 if(*(str1 + i) == *(str2 + j)){ 7 ++ i; 8 ++ j; 9 } 10 else{ 11 i = i - j + 1; //i 回溯到上一輪的下一個 12 j = 0; //j 從第一個開始比較 13 } 14 } 15 if(*(str2 + j) == '\0') 16 return i - j; 17 else 18 return -1; 19 }
缺點:低效,復雜度O(M*N)
但在某些場合,如文本編輯啊,效率也較高,但對於計算機的二進制文件就顯得蒼白無力。
二、KMP算法
所以這個時候KMP算法誕生了,由於是三個人提出了的,所以用了三個人的名字的開頭字母作為名稱,我只記得Knuth,這個人實在太有名了,計算機科學的鼻祖,計算機所有獎項都拿過。
KMP算法是對BF算法的改進,當匹配失效是指針不回溯,根據失效函數(即Next[n]的值)進行下一輪的匹配。
E.g: 主串 “a b a b c a b c a c b a b” 模式串 “a b c a c”
第一趟匹配: a b a b c a b c a c b a b i = 2 i 不回溯
a b c j = 2
第二趟匹配: a b a b c a b c a c b a b i= 6
a b c a c j = 4
第三趟匹配:a b a b c a b c a c b a b i= 10
a b c a c j = 5
依上所得:用數學的語言表述:
假設主串:S1S2.....Sn 模式串:P1P2......Pm
當主串中第 i 個字符與模式串第 k 個字符不匹配,前提是前面的字符皆已匹配,則有下面的關系:
P1P2......P(k-1) = S(i - k + 1)S(i - k + 2)......S(i - 1); ...................................(1)
而對於模式串,有如下關系:
P(j - k + 1)P(j - k + 2)......P(j - 1) = S(i - k + 1)S(i - k + 2)......S(i - 1);..........(2)
根據(1)(2),得:
P1P2......P(k-1) = P(j - k + 1)P(j - k + 2)......P(j - 1);
即最終只需要在模式串中進行比較,這個比較就是計算Next[j]的值,用此作為模式串的指針回溯點。下面會介紹到。
根據以上的推導就可以宏觀地寫出KMP的算法的實現:
1 int KMP(const char * str1, const char * str2){ 2 assert(NULL != str1 && NULL != str2); 3 int i = 0, j = 0; 4 int next[SIZE]; 5 get_next(str2,next); //得到next[j]的值 6 7 while(*(str1 + i) && *(str2 + j)){ 8 if(*(str1 + i) == *(str2 + j)){ 9 ++ i; 10 ++ j; 11 } 12 else{ 13 j = next[j]; //i 不用回溯,j 取得next[j],進行下一輪比較 14 } 15 } 16 17 if(*(str2 + j) == '\0') 18 return i - j; 19 else 20 return 0; 21 }
所以,這個算法最終化為小問題,即求Next[j] 的值。
對於Next[j]的數學推導:
令Next[j] = k,則Next[j] 表明當模式中第 j 個字符與主串 中相應字符失效時,在模式中需重新和主串中該字符進行比較的字符的位置,由此可引出Next[j]函數的定義:
Next[j] = -1 , 當 j = 1時;
= Max{k | 1 < k < j && P1P2......P(k - 1) = P(j - k + 1)......P(j - 1) }
= 0 其他情況;
E.g: j : 0 1 2 3 4 5 6 7
模式串: a b a a b c a c
Next[j]: -1 0 0 1 1 2 0 1 0
很好計算,關鍵是不單要知其為,更要知其所以為。所以理解本質很重要。
下面,看一個例子就懂了:
假設:主串: “a c a b a a b a a b c a c a a b c” 模式串:“a b a a b c a c”
第一趟:a c a b a a b a a b c a c a a b c i = 1;
a b j = 1; Next[j] = 0;
第二趟:a c a b a a b a a b c a c a a b c i = 1;
a j = 0; Next[j] = -1 //模式串中的第一個元素不想等,i 后移;
第三趟:a c a b a a b a a b c a c a a b c i = 7
a b a a b c j = 5; Next[j] = 2;
第四趟:a c a b a a b a a b c a c a a b c i = 13;
a b a a b c a c j = 8; END!
了解到這里,就很容易寫出一份很好的代碼了:
1 void get_next1(const char * str, int Next[]){ 2 assert(NULL != str); 3 int j = Next[0] = -1; 4 int i = 0; 5 while(*(str + i)){ 6 if(-1 == j || *(str + i) == *(str + j)){ //當 j = -1時,有模式串的第一個元素開始比較 7 ++ i; 8 ++ j; 9 } 10 else 11 j = Next[j]; //上一輪比較的next[j]和下一輪將要比較的呈遞增的關系,可以進行簡單的數學推導 12 } 13 }
Note:還未完,下面的很重要
前面定義的Next[]函數在某些情況下有缺陷。E.g:模式串“aaaab”在和主串“aaabaaaab”匹配時,當i = 3, j = 3 時,不等,但由Next[j]的指示還需進行 i = 3, j = 2 ; i = 3, j = 1; i = 3, j = 0這三次的比較,實際上,因為模式串中第1,2,3個字符和第四個字符都相等,因此不需要再和主串第4個字符相比較,而可以直接將模式串一氣像右滑動4個字符。這就是說,若按上述定義得到的Next[j] = k,而模式串中Pj = Pk ,則當主串中字符Si 和 Pj 比較不等時,不需要再和Pk進行比較,而直接和P(Next[k]) 進行比較,有點繞啊,那就 看一個實例吧:
E.g: j : 0 1 2 3 4
模式串: a a a a b
Next[j]: -1 0 1 2 3 0
Next2[j]: -1 0 0 0 3 0
換句話說就是:此時的Next[j] 應該和Next[k] 相同。由此可計算Next函數修正值的算法如下,此時匹配算法不變。
1 void get_next(const char * str, int next[]){ 2 assert(NULL != str); 3 int j = next[0] = -1; 4 int i = 0; 5 6 while(*(str + i)){ 7 if(-1 == j || *(str + i) == *(str + j)){ 8 ++ i; 9 ++ j; 10 if(*(str + i) != *(str + j)){ //先判斷,不等才執行next[i] = j 11 next[i] = j; 12 } 13 else{ 14 next[i] = next[j]; //把前一個next[j]賦給后面的 15 } 16 } 17 else 18 j = next[j]; 19 } 20 }
綜合以上,匯總一下代碼,全部代碼已經嚴格測試通過。
1 /******************************************************* 2 * strstr 3 * there are two algorithm : BF and KMP 4 * compare with the difference of two 5 *******************************************************/ 6 7 const int SIZE = 10; 8 int KMP(const char * str1, const char * str2); 9 int BF(const char * str1, const char * str2) ; 10 void get_next(const char * str,int next[]); 11 void get_next1(const char * str,int next[]); 12 13 int BF(const char * str1, const char * str2){ 14 assert(NULL != str1 && NULL != str2); 15 int i = 0, j = 0; 16 17 while(*(str1 + i) && *(str2 + j)){ 18 if(*(str1 + i) == *(str2 + j)){ 19 ++ i; 20 ++ j; 21 } 22 else{ 23 i = i - j + 1; //i 回溯到上一輪的下一個 24 j = 0; //j 從第一個開始比較 25 } 26 } 27 if(*(str2 + j) == '\0') 28 return i - j; 29 else 30 return -1; 31 } 32 33 int KMP(const char * str1, const char * str2){ 34 assert(NULL != str1 && NULL != str2); 35 int i = 0, j = 0; 36 int next[SIZE]; 37 get_next(str2,next); //得到next[j]的值 38 39 while(*(str1 + i) && *(str2 + j)){ 40 if(*(str1 + i) == *(str2 + j)){ 41 ++ i; 42 ++ j; 43 } 44 else{ 45 j = next[j]; //i 不用回溯,j 取得next[j],進行下一輪比較 46 } 47 } 48 49 if(*(str2 + j) == '\0') 50 return i - j; 51 else 52 return 0; 53 } 54 55 void get_next(const char * str, int next[]){ 56 assert(NULL != str); 57 int j = next[0] = -1; 58 int i = 0; 59 60 while(*(str + i)){ 61 if(-1 == j || *(str + i) == *(str + j)){ 62 ++ i; 63 ++ j; 64 if(*(str + i) != *(str + j)){ //先判斷,不等才執行next[i] = j 65 next[i] = j; 66 } 67 else{ 68 next[i] = next[j]; //把前一個next[j]賦給后面的 69 } 70 } 71 else 72 j = next[j]; 73 } 74 } 75 76 void get_next1(const char * str, int Next[]){ 77 assert(NULL != str); 78 int j = Next[0] = -1; 79 int i = 0; 80 while(*(str + i)){ 81 if(-1 == j || *(str + i) == *(str + j)){ //當 j = -1時,有模式串的第一個元素開始比較 82 ++ i; 83 ++ j; 84 } 85 else 86 j = Next[j]; //上一輪比較的next[j]和下一輪將要比較的呈遞增的關系,可以進行簡單的數學推導 87 } 88 }
歡迎指正交流!
開了個公眾號「aCloudDeveloper」,專注技術干貨分享,期待與你相遇。