問題描述:在一組字符串中,找到所有具有某個字符串前綴字符串,比如application、apple、eyes、cats等。如果要匹配的字符串是app,則符合匹配條件的有application、apple。
思路:首先采用快排將所有字符串進行字典序排序,這樣具有同種前綴的所有字符串都會排在一塊,如果給定一個要匹配的前綴字符串,我們只要找到具有這一字符串前綴的首個字符串下標和末個字符串下標即可,兩個下標之間所有的字符串都會滿足匹配要求。
下面我們的問題是如何找到首個下標和末個下標。字符串已經字典序排好序,我們只要在排好序中二分查找“app”這個前綴字符串,當然不一定存在“app”字符串,但是會找到首個滿足“app”前綴匹配的字符串,查找的條件是:
1.當前比較字符串大於或等於匹配字符串“app”;
2.當前字符串滿足前綴匹配;
3.前一個字符串小於匹配字符串“app”。
這樣就能保證當前比較的字符串是滿足匹配的首個字符串,但是也要處理特殊情況,比如,如果當前比較字符串下標為0,就不能取前一個字符串,或下標為0時還是小於匹配字符串。
同樣,查找末個滿足匹配字符串的下標時,只要在首個下標之后找即可,而且后面的字符串肯定大於匹配字符串“app”,同樣用二分法查找,查找的條件是:
1.只判斷字符串是否滿足前綴匹配;
2.如果當前查找字符串能夠滿足前綴匹配,且后一個字符串不滿足前綴匹配,那么當前字符串下標即為末個下標。
二分法代碼實現:
#include <stdio.h> #include <stdlib.h> #include <string.h> void swap(char **str1,char **str2){ char *temp = *str1; *str1 = *str2; *str2 = temp; } //字典排序 //這里實現一個可以按照按照前len個字符比較字符串 //當len=-1時,比較全部字符,len>0比較前len個字符 //str1>str2 返回1;str1 < str2 返回-1; str1 == str2 返回 0 int strcomp(char* str1, char*str2,int len) { while(*str1 && *str2 && *str1==*str2 && len != 0) { str1++; str2++; len--; } return *str1-*str2; } //快速排序 //先將無序的字符串數組按照字典序排序 int partition(char** set, int start_index, int end_index){ if(set == NULL) return -1; char* temp = set[end_index]; int low = start_index; int high = start_index+1; for(;high<end_index;high++){ if(strcomp(set[high], temp,-1) < 0){ low++; swap(&set[low],&set[high]); } } low++; swap(&set[low], &set[high]); return low; } void quick_sort(char** set, int start_index, int end_index){ if(start_index>= end_index) return; int mid = partition(set, start_index, end_index); quick_sort(set, start_index, mid-1); quick_sort(set, mid+1, end_index); } //二分法找出符合前綴匹配的首個字符串索引 //比較方式:全字符比較 //如果當前中間字符串小於要匹配字符串,繼續在中間字符串后面查找 //如果當前中間字符串大於或等於匹配字符串,此時在判斷中間字符串前一個字符串是否小於匹配字符串,如果小於,則當前字符串就是匹配成功的首個字符串 //不小於,繼續在中間字符串前面查找 int first_binary_search(char** set, int low, int high, char *search_str) { while(low <= high) { int middle = (low + high)/2; if(strcomp(set[middle], search_str,-1) < 0) { low = middle + 1; } else if(strcomp(set[middle],search_str,-1) >= 0) { if(strcomp(set[middle-1], search_str,-1) < 0) return middle; else high = middle - 1; } } //沒找到 return -1; } //二分法找出符合前綴匹配的最后一個字符串索引 //比較方式:前n個字符比較,即判斷查找字符串是否具有前綴匹配 //如果當前中間字符串等於要匹配的字符串,此時在判斷中間字符串后一個字符串是否大於匹配字符串,如果大於,則當前字符串就是匹配成功的最后一個字符串 //不大於,繼續在中間字符串后面查找 //如果當前中間字符串大於匹配字符串,繼續在中間字符串前面查找 int end_binary_search(char** set, int low,int high, char *search_str) { int cmp_len = strlen(search_str)-1; while(low <= high) { int middle = (low + high)/2; if(strcomp(set[middle], search_str,cmp_len) ==0 ) { if(strcomp(set[middle+1], search_str,cmp_len)> 0) return middle; else low = middle + 1; } else if(strcomp(set[middle],search_str,cmp_len) > 0) { high = middle - 1; } } //沒找到 return -1; } int main(){ char *set[] = {"application","apple","apply","eyes","attation"}; //先將字符串數組字典排序 quick_sort(set,0,4); for(int i = 0;i<5;i++) { printf("%s ", set[i]); } printf("\n"); char pre[] = "ab"; //計算得到符合前綴匹配的首個字符串下標和最后一個字符串下標 int first_index = first_binary_search(set,0,4,pre); int end_index = end_binary_search(set,first_index,4,pre); if(first_index == -1 || end_index == -1) printf("未找到匹配的字符串"); else { for(int i = first_index;i<= end_index;i++) printf("%s ", set[i]); } }
當然,隨着字符串數量的增多(1000000個),和動態的插入和刪除,上述排序查找的方法就有些效率問題了,這時我們可以采用Trie樹組織存儲所有的字符串,在網上查了一下資料,研究了一下Trie樹,覺得Tire樹對字符串匹配會有更好的性能,能比較好的支持插入刪除,且查找和匹配的性能也很好。
參考鏈接:http://blog.csdn.net/hguisu/article/details/8131559
- 根節點不包含字符,除根節點外每一個節點都只包含一個字符。
- 從根節點到某一節點,路徑上經過的字符連接起來,為該節點對應的字符串。
- 每個節點的所有子節點包含的字符都不相同。
- 和二叉查找樹不同,在trie樹中,每個結點上並非存儲一個元素。
- trie樹把要查找的關鍵詞看作一個字符序列。並根據構成關鍵詞字符的先后順序構造用於檢索的樹結構。
- 在trie樹上進行檢索類似於查閱英語詞典。
- 一棵m度的trie樹或者為空,或者由m棵m度的trie樹構成。
4.插入過程
對於一個單詞,從根開始,沿着單詞的各個字母所對應的樹中的節點分支向下走,直到單詞遍歷完,將最后的節點標記為紅色,表示該單詞已插入trie樹。
5.查找過程
- 從根結點開始一次搜索;
- 取得要查找關鍵詞的第一個字母,並根據該字母選擇對應的子樹並轉到該子樹繼續進行檢索;
- 在相應的子樹上,取得要查找關鍵詞的第二個字母,並進一步選擇對應的子樹進行檢索。
- 迭代過程……
- 在某個結點處,關鍵詞的所有字母已被取出,則讀取附在該結點上的信息,即完成查找。其他操作類似處理。
即從根開始按照單詞的字母順序向下遍歷trie樹,一旦發現某個節點標記不存在或者單詞遍歷完成而最后的節點未標記為紅色,則表示該單詞不存在,若最后的節點標記為紅色,表示該單詞存在。如下圖中:trie樹中存在的就是abc、d、da、dda四個單詞。在實際的問題中可以將標記顏色的標志位改為數量count等其他符合題目要求的變量。
采用Trie樹,實現上述的字符串前綴匹配算法
代碼實現:
#include <iostream> #include <string.h> using namespace std; static const int branchNum = 26; //聲明常量 static const int Max_Word_Len = 40; //聲明常量 static int i; static int pos = 0; char worddump[Max_Word_Len+1]; struct Trie_node { //記錄此處是否構成一個串。 bool isStr; //指向各個子樹的指針,下標0-25代表26字符 Trie_node *next[branchNum]; Trie_node():isStr(false) { for(int i = 0; i<branchNum; i++) next[i] =NULL; } }; class Trie { public: Trie(); void insert(const char* word); void search(const char* word); int traverse(Trie_node *result,int i); void deleteTrie(Trie_node *root); private: Trie_node* root; }; Trie::Trie() { root = new Trie_node(); } void Trie::insert(const char *word) { Trie_node *location = root; while(*word) { if(location->next[*word-'a'] == NULL) { Trie_node *tmp = new Trie_node(); location->next[*word-'a'] = tmp; } location = location->next[*word-'a']; word++; } //到達尾部,即為一個字符串 location->isStr = true; } int Trie::traverse(Trie_node *result,int char_i) { if (result == NULL) return 0; if (result->isStr) { worddump[pos]='a'+char_i; worddump[pos+1]='\0'; printf("%s\n", worddump); } else if(char_i>=0) { worddump[pos]='a'+char_i; } for (int i=0; i<branchNum; ++i) { pos++; traverse(result->next[i],i); pos--; } return 0; } void Trie::search(const char *word) { Trie_node *location = root; const char *ptr = word; while(*ptr && location) { location = location->next[*ptr-'a']; ptr++; } if(location != NULL && !(*ptr)) { ptr = word; int pre_len = strlen(ptr); while(*ptr) worddump[pos++] = *ptr++; pos--; traverse(location,-1); } else { printf("no vaild word\n"); } } void Trie::deleteTrie(Trie_node *root) { for(i = 0; i < branchNum; i++) { if(root->next[i] != NULL) { deleteTrie(root->next[i]); } } delete root; } int main() { Trie t; char *set[] = {"application","apple","apply","eyes","attation"}; for(int i=0;i<5;i++) t.insert(set[i]); char pre[] = "app"; t.search(pre); return 0; }