字符串匹配数据结构 --Trie树 高效实现搜索词提示 / IDE自动补全
文章目錄
- 1. 算法背景
- 2. Trie 樹實現原理
- 2.1 Trie 樹的構建
- 2.2 Trie樹的查找
- 2.3 Trie樹的遍歷
- 2.4 Trie樹的時間/空間復雜度
- 2.5 Trie 樹 Vs 散列表/紅黑樹
- 3. Trie樹的應用 -- 搜索詞提示功能
1. 算法背景
之前我們了解過單模式串匹配的相關高效算法 – BM/KMP,雖難以理解,缺能夠給予我們足夠的寬度來擴展思維。
1. BF 和 RK 算法實現
2. BM 和 KMP 算法詳解
但單模式串的匹配僅僅限于一個模式串從一個主串中查找,實際場景中我們卻需要從多個主串中查找模式串,像IDE/文本編輯器甚至搜索引擎這樣的龐大的數據量下多模式串中的高效查找卻是單模式串查找效率無法滿足的。
基于多模式串的高效搜索能力是需要我們重點關注的方向,也就是我們今天要推出的Tire 樹 數據結構。
Trie 樹能夠比較友好得實現搜索詞提示功能,接下來詳細看看Trie樹的原理。
2. Trie 樹實現原理
Trie樹 的核心目的是為了讓擁有公共前綴的主串能夠以一個樹的形態存在。就像樹有主干分支,有葉子分支一樣,trie樹讓公共前綴的部分形成主干分支,沒有公共前綴的部分就各自形成葉子分支。
這樣的一個字符串樹能夠極大得方面模式串的查找,順著主干分支直接移動模式串的下標匹配,非主干分支也能夠快速匹配。而不需要像單模式串那樣,為了加速匹配的過程還需要考慮前綴/后綴子串。
2.1 Trie 樹的構建
Trie 樹的大體形態如下:
將:adas,ada,adf,am,ao,aok,dk,dqe,deq 這樣的獨立主串中的字符組織成一個個樹的節點,從樹的根節點開始,沿著主干分支即可能夠快速確認模式串是否能夠匹配。
Trie樹中不一定是葉子結點才是一個字符串的結束字符,葉子節點中間也可能有結束字符。
所以構建trie樹的過程需要為上下節點之間建立連接關系,從而保證查找能夠從上一個節點準確得落到下一個節點。
構建形態如下:
為每一個節點維護一個字符串全集的數組,比如 am這個字符串,在a節點處維護一個26長度的節點數組,其中a字符串所在下標不為空且指向下一個字符數組中的m,而b,c,d…等其他字符的數組為空即可。
ps : 這里大家也能夠發現一個問題,就是構建Trie樹的過程需要消耗大量的空間,雖然有公共前綴的公共存儲,但是對于一個字符存儲來說,需要26個額外的指針空間,所以Trie樹的內存消耗問題顯而易見。
定義TrieNode節點如下:
// Trie nodeinfo
class TrieNode {public:char data_;TrieNode *children_[26];bool isEndingChar_;TrieNode(char data='/') :data_(data),isEndingChar_(false){memset(children_, 0, sizeof(TrieNode *)* 26);};
};
構建的主要過程如下:
- 主串數組逐個交給初始化后的根節點
- 根節點逐個遍歷輸入主串的字符:
- 確認每個字符所處下一層的children_數組中的位置(因為這里是26個字母,index = input[i] - ‘a’)
- 核對下一層的children數組是否為空,不為空則表明是公共前綴,繼續處理下一個輸入字符
- 為空 則說明需要為當前input[i]構建一個新的TrieNode添加進來
- 完成將一個輸入主串的所有字符添加到Trie樹之后 更新結尾標記(表示當前位置為這個主串的結尾標記)。
void Trie::insert(string des) {if (des.size() <= 0) {return;}TrieNode *tmp = root_;int i;// Traverse every character in desfor (i = 0;i < des.size(); i++) {// The des[i] insert position at trie tree.int index = des[i] - 'a';if (tmp->children_[index] == nullptr) {TrieNode *newNode = new TrieNode(des[i]);tmp->children_[index] = newNode; }tmp = tmp->children_[index];}tmp->isEndingChar_ = true;
}
2.2 Trie樹的查找
完成了Trie樹的構建,剩下的查找就比較容易了。
- 拿著輸入的字符串逐個字符遍歷,確認每一個字符的index
- 如果這個字符index對應的TrieNode為空,且這個字符不是整個字符串的最后一個字符,則說明不匹配
- 如果不為空,則說明Trie樹中有這個節點,那表示當前字符匹配,繼續后續字符的處理
- 當最后一個字符對應的TrieNode中的End標記為真,則說明字符串匹配;否則不匹配
代碼如下:
// Judge if a string is match with Trie tree
bool Trie::find(string des) {if (des.size() == 0) {return false;}TrieNode *tmp = root_;int i;for (i = 0;i < des.size(); i++) {// The index of the current char's positionint index = des[i] - 'a';if (tmp->children_[index] == nullptr) {return false;}// Move the tmp to the next linetmp = tmp->children_[index];}// End position to ensure wether the input str is match.if (tmp->isEndingChar_ == false) {return false;}return true;
}
2.3 Trie樹的遍歷
Trie樹的遍歷就是一個深搜的過程,沿著一個方向直接找到最后一個節點即可。
// Traverse the trie tree recursion
// para1: TrieNode
// Para2: prefix string
// para3: result vector
void Trie::dfs_traverse(TrieNode *p, string buf, vector<string> &tmp_str) {if (p == nullptr) {return;}// if match, just and the result to vectorif (p->isEndingChar_ == true) {tmp_str.push_back(buf);}for (int i = 0; i < 26; i++) {if (p->children_[i] != nullptr) {// Just add the prefix every timedfs_traverse(p->children_[i], buf+(p->children_[i]->data_), tmp_str);}}
}// Print the all trie tree string with dictionary order
void Trie::printTrie() {vector<string> tmp_str;int i, j;for (i = 0;i < 26; i++) {string buff = "";if (root_->children_[i] != nullptr) {// Will be called recursion.// Input with TrieNode, the prefix character and the result vectordfs_traverse(root_->children_[i], buff + root_->children_[i]->data_, tmp_str);}}cout << "Trie string: " << tmp_str.size() << endl;for (j = 0;j < tmp_str.size(); j++) {cout << tmp_str[j] << endl;}
}
2.4 Trie樹的時間/空間復雜度
- 空間復雜度:空間消耗不用說,對于總共n個字符的所有主串來說,上僅僅是26個字母,以上為每一個字符都實現了一個26位的指針數組。64位機器下的最壞空間消耗:(26*8 + 1)*n B,顯然Trie樹的空間消耗是一個非常大的問題。當然對于公共前綴比較多的場景,構建Trie的空間會一定程度的降低。
- 時間復雜度:構建Trie樹需要遍歷 n個字符中的每一個字符 消耗O(n);構建好Trie樹之后,每一個模式串的匹配同樣只需要遍歷一次 消耗O(k),整個時間復雜度是O(k+n)。
2.5 Trie 樹 Vs 散列表/紅黑樹
| Trie樹(26字符) | 散列表/紅黑樹 | |
|---|---|---|
| 內存消耗 | 26*8*n | O(n) |
| 查找效率 | O(n+k) | O(1) |
| 工業實現 | 無,需手動實現 | 有且完備 |
| 適用場景 | 搜索詞提示/IDE自動補全 | 字符串精確查找 |
綜上,如果需要多模式串的精確功能,紅黑樹/散列表等工業實現會更合適;如果需要搜索詞提示這樣的功能,則Trie樹的結構天然適合。
以上完整測試代碼:
#include <iostream>
#include <string>
#include <vector>using namespace std;// Trie nodeinfo
class TrieNode {public:char data_;TrieNode *children_[26];bool isEndingChar_;TrieNode(char data='/') :data_(data),isEndingChar_(false){memset(children_, 0, sizeof(TrieNode *)* 26);};
};// Trie tree info with a root node
class Trie {
public: Trie() {root_ = new TrieNode();}~Trie() {destory(root_);}void insert(string des);bool find(string des);void printTrie();void destory(TrieNode *p);void dfs_traverse(TrieNode *p, string buf, vector<string> &tmp_str);private:TrieNode *root_;
};// Delete the TrieNode, and release the space
void Trie::destory(TrieNode *p) {if (p == nullptr) {return;}for (int i = 0;i < 26; i++) {destory(p->children_[i]);}delete p;p = nullptr;
}void Trie::insert(string des) {if (des.size() <= 0) {return;}TrieNode *tmp = root_;int i;for (i = 0;i < des.size(); i++) {// The des[i] insert position at trie tree.int index = des[i] - 'a';if (tmp->children_[index] == nullptr) {TrieNode *newNode = new TrieNode(des[i]);tmp->children_[index] = newNode; }tmp = tmp->children_[index];}tmp->isEndingChar_ = true;
}// Traverse the trie tree recursion
void Trie::dfs_traverse(TrieNode *p, string buf, vector<string> &tmp_str) {if (p == nullptr) {return;}// if match, just and the result to vectorif (p->isEndingChar_ == true) {tmp_str.push_back(buf);}for (int i = 0; i < 26; i++) {if (p->children_[i] != nullptr) {// Just add the prefix every timedfs_traverse(p->children_[i], buf+(p->children_[i]->data_), tmp_str);}}
}// Print the trie tree with dictionary order
void Trie::printTrie() {vector<string> tmp_str;int i, j;for (i = 0;i < 26; i++) {string buff = "";if (root_->children_[i] != nullptr) {// Will be called recursiondfs_traverse(root_->children_[i], buff + root_->children_[i]->data_, tmp_str);}}cout << "Trie string: " << tmp_str.size() << endl;for (j = 0;j < tmp_str.size(); j++) {cout << tmp_str[j] << endl;}
} // Judge if a string is match with Trie tree
bool Trie::find(string des) {if (des.size() == 0) {return false;}TrieNode *tmp = root_;int i;for (i = 0;i < des.size(); i++) {// The index of the current char's positionint index = des[i] - 'a';if (tmp->children_[index] == nullptr) {return false;}// Move the tmp to the next linetmp = tmp->children_[index];}// End position to ensure wether the input str is match.if (tmp->isEndingChar_ == false) {return false;}return true;
}int main() {string s[5] = {"adafs", "dfgh", "amkil", "doikl", "aop"};Trie *trie = new Trie();for (int i = 0; i < 5; i++) {trie->insert(s[i]);}trie->printTrie();string in_str;cout << "Inpunt a string :" << endl;cin >> in_str;if (trie->find(in_str)) {cout << "Trie tree has the str: " << in_str << endl;} else {cout << "Trie tree doesn't have the str : " << in_str << endl;}return 0;
}
輸出如下:
> ./trie_alg
Trie string: 5
adafs
amkil
aop
dfgh
doiklInpunt a string :
aoe
Trie tree doesn't have the str : aoe
3. Trie樹的應用 – 搜索詞提示功能
想要實現搜索詞提升這樣的功能,需要基于Trie樹實現做一些邏輯的添加。比如 用戶輸入h,則能夠返回h為開頭的字符串;輸入he,則能夠返回he開頭的字符。。。
類似如下:
source code: https://github.com/BaronStack/DATA_STRUCTURE/blob/master/string/trie_alg.cc
實現邏輯如下:
// Traverse the trie tree recursion
void Trie::dfs_traverse(TrieNode *p, string buf, vector<string> &tmp_str) {if (p == nullptr) {return;}// if match, just and the result to vectorif (p->isEndingChar_ == true) {tmp_str.push_back(buf);}for (int i = 0; i < 26; i++) {if (p->children_[i] != nullptr) {// Just add the prefix every timedfs_traverse(p->children_[i], buf+(p->children_[i]->data_), tmp_str);}}
}// Input the prefix, and search the prefix related string
void Trie::printTrieWithPrefix(string start) {vector<string> tmp_str;int i, j;TrieNode *tmp = root_;// Ensure prefix is existfor (int i = 0;i < start.size(); i++) {int index = start[i] - 'a';if (tmp->children_[index] == nullptr) {cout << "No prefix with " << start << endl;return;} tmp = tmp->children_[index];}// Prefix is a matched stringtmp_str.push_back(start);for (i = 0;i < 26; i++) {string buff = start;if (tmp->children_[i] != nullptr) {// Will be called recursiondfs_traverse(tmp->children_[i], buff + tmp->children_[i]->data_, tmp_str);}}cout << "Trie string: " << tmp_str.size() << endl;for (j = 0;j < tmp_str.size(); j++) {cout << tmp_str[j] << endl;}
}
輸出如下:
Trie string: 5
adafs
amkil
aop
dfgh
doikl
Input a prefix :
a
Trie string: 3
adafs
amkil
aop
同樣,如果想要為每個字符串增加更多的指標 – 比如公共前綴重合度 這樣的屬性可以增加到TrieNode數據結構中,在構建Trie樹的過程中為每一個TrieNode的這個屬性做對應的變更即可。
總結
以上是生活随笔為你收集整理的字符串匹配数据结构 --Trie树 高效实现搜索词提示 / IDE自动补全的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 妄想山海岩浆里的蛋怎么捡?
- 下一篇: 字符串匹配算法 -- AC自动机 基于T