字符串匹配是計算機的基本任務之一。
舉例來說,有一個字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一個字符串"ABCDABD"?
下面的的KMP算法的解釋步驟,引用於http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一個字符與搜索詞"ABCDABD"的第一個字符,進行比較。因為B與A不匹配,所以搜索詞后移一位。
2.
因為B與A不匹配,搜索詞再往后移。
3.
就這樣,直到字符串有一個字符,與搜索詞的第一個字符相同為止。
4.
接着比較字符串和搜索詞的下一個字符,還是相同。
5.
直到字符串有一個字符,與搜索詞對應的字符不相同為止。
6.
這時,最自然的反應是,將搜索詞整個后移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把"搜索位置"移到已經比較過的位置,重比一遍。
7.
一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"。KMP算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向后移,這樣就提高了效率。
8.
怎么做到這一點呢?可以針對搜索詞,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,后面再介紹,這里只要會用就可以了。
9.
已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最后一個匹配字符B對應的"部分匹配值"為2,因此按照下面的公式算出向后移動的位數:
移動位數 = 已匹配的字符數 - 對應的部分匹配值
因為 6 - 2 等於4,所以將搜索詞向后移動4位。
10.
因為空格與C不匹配,搜索詞還要繼續往后移。這時,已匹配的字符數為2("AB"),對應的"部分匹配值"為0。所以,移動位數 = 2 - 0,結果為 2,於是將搜索詞向后移2位。
11.
因為空格與A不匹配,繼續后移一位。
12.
逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜索詞向后移動4位。
13.
逐位比較,直到搜索詞的最后一位,發現完全匹配,於是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向后移動7位,這里就不再重復了。
14.
下面介紹《部分匹配表》是如何產生的。
首先,要了解兩個概念:"前綴"和"后綴"。 "前綴"指除了最后一個字符以外,一個字符串的全部頭部組合;"后綴"指除了第一個字符以外,一個字符串的全部尾部組合。
15.
"部分匹配值"就是"前綴"和"后綴"的最長的共有元素的長度。以"ABCDABD"為例,
- "A"的前綴和后綴都為空集,共有元素的長度為0;
- "AB"的前綴為[A],后綴為[B],共有元素的長度為0;
- "ABC"的前綴為[A, AB],后綴為[BC, C],共有元素的長度0;
- "ABCD"的前綴為[A, AB, ABC],后綴為[BCD, CD, D],共有元素的長度為0;
- "ABCDA"的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A],共有元素為"A",長度為1;
- "ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA],后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;
- "ABCDABD"的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB],后綴為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。
16.
"部分匹配"的實質是,有時候,字符串頭部和尾部會有重復。比如,"ABCDAB"之中有兩個"AB",那么它的"部分匹配值"就是2("AB"的長度)。搜索詞移動的時候,第一個"AB"向后移動4位(字符串長度-部分匹配值),就可以來到第二個"AB"的位置。
接下來,就是我自己對KMP算法的實現了。
這個算法的實現主要包括了三個方面:
1) 求得我們用來搜索字符串的部分匹配值表
2) 實現待搜索字符串在搜索過程中的指針的移動問題
3) 如何定位我們搜索到的結果
接下來我就貼上我實現的代碼
1 /* 2 *用KMP算法實現字符串匹配搜索方法 3 *該程序實現的功能是搜索本目錄下的所有文件的內容是否與給定的 4 *字符串匹配,如果匹配,則輸出文件名:包含該字符串的行 5 *待搜索的目標串搜索指針移動位數 = 已匹配的字符數 - 對應部分匹配值 6 */ 7 8 #include <stdio.h> 9 #include <string.h> 10 #include <stdlib.h> 11 12 #define KEYWORD_MAX_LENGTH 100 //設定搜索串的最大長度 13 14 int kmp_table[KEYWORD_MAX_LENGTH]; //為搜索串建立kmp表 15 char prefix_stack[KEYWORD_MAX_LENGTH]; //前綴表達式棧 16 char suffix_stack[KEYWORD_MAX_LENGTH]; //后綴表達式棧 17 int keyword_length = 0; //搜索串的長度 18 int record_position[KEYWORD_MAX_LENGTH]; //記錄與關鍵字串匹配源串中的位置 19 20 /* 21 *GetMatchValue:獲得字符串src的部分匹配值 22 */ 23 int GetMatchValue(char *src) 24 { 25 int value = 0; 26 int src_len = strlen(src); 27 char *begin = src; //初始化指向字符串第一個字符 28 char *end = src + (src_len - 1); //初始化指向字符串最后一個字符 29 int i = 0; 30 for(i=0;i<(src_len-1);i++) 31 { 32 prefix_stack[i] = *begin; 33 suffix_stack[i] = *end; 34 begin++; 35 end--; 36 } 37 char *p = prefix_stack; 38 char *q = suffix_stack + (src_len - 2); //指向棧中最后一個元素 39 int flag = 0; //用一個標志位來確定后綴棧中到最后一個元素都與前綴棧中的符號匹配 40 while(q >= suffix_stack) 41 { 42 if(*p == *q) 43 { 44 value++; 45 p++; 46 flag=1; 47 } 48 else { 49 flag = 0; 50 } 51 q--; 52 } 53 if(flag == 0) value = 0; 54 return value; 55 } 56 57 /* 58 *創建搜索字符串的KMP表 59 */ 60 int Create_KMP_Table(char *str,int *table) 61 { 62 int i; 63 char *dst; 64 keyword_length = strlen(str); 65 for(i=0;i<keyword_length;i++) 66 { 67 if(i == 0) { 68 table[i] = 0; //第一個字符無前綴和后綴,所以為0 69 } 70 else { 71 dst = (char*)malloc((i+2)); 72 if(dst == NULL) 73 { 74 printf("malloc space error!\n"); 75 return EXIT_FAILURE; 76 } 77 strncpy(dst,str,(i+1)); //匹配str的前(i+1)個字符 78 dst[i+1] = '\0'; //注意字符串要以'/0'結尾 79 table[i] = GetMatchValue(dst); 80 free((void*)dst); 81 } 82 } 83 return EXIT_SUCCESS; 84 } 85 86 //打印搜索字符串對應的KMP表 87 void Table_Print(char *str,int *table) 88 { 89 int i; 90 char c = *str; 91 while(c != '\0') 92 { 93 printf("%-4c",c); //左對齊輸出搜索字符串中的字符 94 c = *++str; 95 } 96 printf("\n"); 97 for(i=0;i<keyword_length;i++) 98 { 99 printf("%-4d",table[i]); //左對齊輸出每個字符對應的部分匹配值 100 } 101 printf("\n"); 102 } 103 104 //在目標串dst_str中搜索關鍵子串search_str,打印出關鍵字串的位置信息,返回與關鍵字串匹配的數目 105 int Search_Keyword(char *dst_str,char *search_str) 106 { 107 char *p = dst_str; 108 char *q = search_str; 109 char *temp; 110 111 //創建關鍵字串的KMP表 112 Create_KMP_Table(search_str,kmp_table); 113 114 int count = 0; //記錄現在已經匹配的數目 115 int k = 0; //記錄與關鍵字串匹配的字串的數目 116 int move = 0; //當字符串不匹配時,搜索指針移動的位數 117 118 while(*p != '\0') //直到搜索到目標串的最后一個字符為止 119 { 120 temp = p; 121 while(*q != '\0') 122 { 123 if(*q == *temp) 124 { 125 count++; 126 temp++; 127 q++; 128 } 129 else break; 130 } 131 132 if(count == 0) 133 p++; 134 else { 135 if(count == keyword_length) 136 { 137 record_position[k++] = (temp-dst_str)-(keyword_length); 138 } 139 move = count - kmp_table[count-1]; 140 p += move; 141 } 142 143 count = 0; 144 q = search_str; 145 } 146 return k; 147 } 148 149 150 int main(int argc,char **argv) 151 { 152 char *search_str = argv[1]; 153 //char dst_str[] = "hello woshijpf woshijpf woshij woshijp woshijpf"; 154 char dst_str[] = "BBC ABCDAB ABCDABCDABDE"; 155 156 printf("Please input serach string and dst_string\n"); 157 if(search_str == NULL) 158 { 159 printf("Please input search string\n"); 160 return EXIT_FAILURE; 161 } 162 163 if(dst_str == NULL) 164 { 165 printf("Please input dst_string\n"); 166 return EXIT_FAILURE; 167 } 168 169 int result = Search_Keyword(dst_str,search_str); //放回搜索到的結果的數目 170 Table_Print(search_str,kmp_table); 171 printf("%s\n",dst_str); //輸出待搜索的目標串 172 if(result == 0) 173 { 174 printf("Sorry!Don't find the string %s\n",search_str); 175 return EXIT_SUCCESS; 176 } 177 else { 178 int i,j,num; 179 int before = 0; 180 for(i=0;i<result;i++) 181 { 182 num = record_position[i] - before; //打印搜索串在目標串中的位置 183 before = record_position[i]+1; 184 for(j=1;j<=num;j++) 185 printf(" "); 186 printf("*"); 187 } 188 printf("\n"); 189 } 190 191 return EXIT_SUCCESS; 192 }
測試的結果: