Trie(字典树) : 如何实现搜索引擎的关键词提示功能?
文章目錄
- 搜索中的關鍵詞提示
- Trie樹的介紹
- Trie樹的實現
- 搜索的關鍵詞提示
- Trie樹 VS 紅黑樹、哈希
搜索中的關鍵詞提示
當我們在搜索引擎中進行搜索時,有時僅僅輸入了搜索內容的一部分,搜索引擎就會提示我們可能的一些選擇,這樣我們就不再需要將查詢詞完整的輸入,大大節約了我們的時間。
而實現這一功能的基石,正是Trie樹
Trie樹的介紹
Trie樹又叫做字典樹、前綴樹。顧名思義,它是一個用于處理多模式字符串匹配的多叉樹,用來在一組字符串中快速的找到某個字符串。
其本質就是共享字符串的公共前綴,即利用字符串之間的公共前綴,將重復的前綴合并在一起。
對于每一個節點來說,它的每個子節點就代表著一種字符,例如我們插入一個hi,其就會在根節點尋找是否存在h節點,如果沒有則新建一個,接著進入h節點,尋找是否存在i,如果沒有也新建一個。接著到了i之后,當前字符串插入結束,為了標識hi是一個完整的單詞而不是前綴,就會在該節點中做一個標記。
如下圖,我們往里面插入幾個字符串,插入流程如下。
此時,如果我們想在里面尋找是否存在hello
此時找到了這個單詞,并且結束的位置o上存在標記,說明這時一個完整的單詞,查找成功
接著查找se
雖然找到了se,但是此時并不存在標記,所以當前的se是前綴也不是單詞,查找失敗。
Trie樹的實現
了解了思路后下面就可以開始實現了
首先進行數據結構的選擇
我們從上面描述的原理可以得知,由于Trie樹需要標記多種不同的字符,所以不可能是二叉樹,而是一個多叉樹,所以我們就需要對子節點的數據結構進行一個選擇
我們可以選用有序數組、哈希表、紅黑樹、跳表等數據結構,例如我們的字典樹中只存在英文字母的時候,就可以選擇用一個大小為26的數組來存儲,例如以下結構,這時很常見的一種實現方法。
但是如果想要實現一個關鍵詞提示功能,字符的范圍絕不限制于字母,還可能有數字、標點符號、各種語言等,所以對于子數組的大小,我們無法得知。
這里我選擇使用哈希表來完成,哈希表可以動態擴容,我們就不必關心字符的種類數量,并且可以用O(1)的時間復雜度來找到一個字符是否存在,大大的提高了效率。
struct TrieNode {char _data; //當前字符bool _isEnd; //標記當前是否能構成一個單詞unordered_map<char, TrieNode*> _subNode; //子節點 };代碼實現如下,具體的實現思路寫在了注釋中。
#include<unordered_map> #include<string> #include<vector>using std::vector; using std::string; using std::unordered_map; using std::make_pair;//Trie樹節點 struct TrieNode {TrieNode(char data = '\0'): _data(data), _isEnd(false){}char _data; //當前字符bool _isEnd; //標記當前是否能構成一個單詞unordered_map<char, TrieNode*> _subNode; //子節點 };//Trie樹 class TrieTree { public:TrieTree(): _root(new TrieNode()){}~TrieTree(){delete _root;}//防拷貝TrieTree(const TrieTree&) = delete;TrieTree& operator=(const TrieTree&) = delete;//插入字符串void insert(const string& str){if (str.empty()){return;}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){//如果找不到該字符,則在對應層中插入if (cur->_subNode.find(str[i]) == cur->_subNode.end()){cur->_subNode.insert(make_pair(str[i], new TrieNode(str[i])));}cur = cur->_subNode[str[i]];}//標記該單詞存在cur->_isEnd = true;}//查找字符串bool find(const string& str){if (str.empty()){return false;}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){if (cur->_subNode.find(str[i]) == cur->_subNode.end()){return false;}cur = cur->_subNode[str[i]];}//如果當前匹配到的是一個前綴而非一個完整的單詞,則返回錯誤return cur->_isEnd == true ? true : false;}//查找是否存在以包含str為前綴的字符串bool startsWith(const string& str){if (str.empty()){return false;}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){if (cur->_subNode.find(str[i]) == cur->_subNode.end()){return false;}cur = cur->_subNode[str[i]];}return true;}//返回所有以str為前綴的字符串vector<string> getPrefixWords(const string& str){if (str.empty()){return {};}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){if (cur->_subNode.find(str[i]) == cur->_subNode.end()){return {};}cur = cur->_subNode[str[i]];}vector<string> res;string s = str;_getPrefixWords(cur, s, res);return res;}private://使用回溯來找到所有包含該前綴的字符串void _getPrefixWords(TrieNode* cur, string& str, vector<string>& res){//如果當前能構成一個單詞,則加入結果集中if (cur->_isEnd){res.push_back(str);}//匹配當前所有可能字符for (const auto& sub : cur->_subNode){str.push_back(sub.first); //匹配當前字符_getPrefixWords(sub.second, str, res); //匹配下一個字符str.pop_back(); //回溯,嘗試其他結果}}TrieNode* _root; //根節點,存儲空字符 };下面進行一個簡單的測試,演示一下上面實現的所有函數
#include<iostream> #include"TrieTree.hpp"using namespace std;int main() {TrieTree trie;trie.insert("hello");trie.insert("helo");trie.insert("hill");trie.insert("world");trie.insert("test");trie.insert("cpp");trie.insert("我");trie.insert("我愛學習");trie.insert("我愛C++");trie.insert("你");//測試中文,查找以我為前綴的所有字符串for (auto str : trie.getPrefixWords("我")){cout << str << endl;}//測試英文,查找以h為前綴的所有字符串for (auto str : trie.getPrefixWords("h")){cout << str << endl;}cout << trie.find("我") << endl;cout << trie.find("它") << endl;cout << trie.find("cpp") << endl;cout << trie.find("java") << endl;cout << trie.startsWith("h") << endl;return 0; }搜索的關鍵詞提示
回到我們開頭說的那種情景,在我們往搜索引擎中輸入字符串的過程時,它就會把這個詞作為一個前綴子串在Trie樹中匹配,并且找到所有滿足條件的結果,這也就是我們實現的getPrefixWords函數的功能,這就是搜索關鍵詞提示的最基本的算法原理。
但是在搜索引擎背后的技術,不僅僅只有這么簡單。當我們在搜索時,即使我們不以前綴輸入,而是輸入其中的一個片段,又或者我們輸入的查詢詞錯誤,他也會校正后返回正確的結果,這又是如何做到的呢?我們是否能將這個功能更加廣泛化,如實現編譯器、輸入法的自動補全等?
在這里就先挖一個坑,或許未來我會寫一篇有關這方面的博客來具體講一講它們背后的原理。
如果想要了解搜索引擎的簡單原理,可以參考我的往期博客,這是C++實現的一個簡單的站內搜索引擎。
【項目介紹】搜索引擎
Trie樹 VS 紅黑樹、哈希
通常來說,在查找問題上我們都會使用紅黑樹和哈希等數據結構,那么和他們比起來,Trie樹有什么優勢嗎?
從上面的實現可以看出,構建一個Trie樹的時間復雜度為O(N * M)(N為字符串數,M為字符串長度,這里可直接視為總字符數),而字符串的查找時間為O(M)
雖然這個效率確實挺高,但是比起上述的數據結構,并沒有什么突出的地方,并且Trie樹還有以下幾種嚴重的缺點
缺點
- 內存消耗大,從上面可以看出來,Trie樹是典型的以時間換空間的做法,為了維護每一個節點的子節點花費了大量的空間。
- 要求字符串的前綴重合多,否則為了維護子節點消耗的空間會變多
- 常見的語言如JAVA、C++庫中都實現了紅黑樹、哈希表,而沒有實現Trie樹,所以需要自己實現
按照上面所描述的,難道Trie真的那么無用嗎?錯了,Trie樹只是不適合那種精確的匹配查找,它的優勢在于查找前綴匹配的字符串,也就是我們開頭的那種場景。
總結
以上是生活随笔為你收集整理的Trie(字典树) : 如何实现搜索引擎的关键词提示功能?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL 锁与MVCC :数据库的锁、
- 下一篇: 字符串匹配算法(一):BF(BruteF