當你在搜索引擎中輸入想要搜索的一部份內容時,搜索引擎就會自動彈出下拉框,裏面是各類關鍵詞提示,這個功能是怎麼實現的呢?其實底層最基本的就是 Trie 樹這種數據結構。
Trie 樹也叫 「字典樹」。顧名思義,它是一個樹形結構,專門用來處理在一組字符串集合中快速查找某個字符串的問題。ios
假設咱們有 6 個字符串,它們分別是:how,hi,her,hello,so,see。咱們但願在這裏面屢次查找某個字符串是否存在,若是每次都拿要查找的字符串和這六個字符串依次進行匹配,那效率就會比較低。算法
若是咱們能夠對這六個字符串作一下預處理,組織成 Trie 樹的結構,那以後每次查找,都只要在 Trie 樹中進行匹配便可。Trie 樹的本質,就是利用字符串之間的公共前綴,將重複的前綴合並在一塊兒。編程
其中,根節點不包含任何信息,每一個節點表明字符串中的一個字符,從根節點到紅色節點的一條路徑表示一個字符串。注意紅色節點並不都是葉子節點,好比有兩個詞 how 和 however,那麼 w 和 r 都是紅色節點。一個 Trie 樹的構造過程以下所示。數組
當咱們要在構建好的 Trie 樹中查找一個字符串的時候,那就要將查找的字符串分割成單個的字符,而後從根節點開始匹配。以下面的例子所示,綠色路徑就是 「her」 的匹配路徑,而 「he」 的最後一個匹配節點並非紅色節點,因此其並不能徹底匹配任何字符串。瀏覽器
從上面咱們能夠看到,Trie 樹主要有兩個操做:一個是將字符串集合構建成 Trie 樹,另外一個是在 Trie 樹中查詢一個字符串。緩存
Trie 樹是一個多叉樹結構,其子節點個數事先未知,但咱們能夠藉助散列表的思想,在下標與字符之間創建一個一一映射,來存儲子節點的指針。數據結構
假設咱們的字符串只有 a 到 z 這 26 個字母,那麼數組下標爲 0 的元素就存儲指向子節點 a 的指針,下標爲 1 的元素就存儲指向子節點 b 的指針,以此類推,下標爲 25 的元素就存儲指向子節點 z 的指針。若是某個字符的子節點不存在,那對應該下標位置的元素就爲 NULL。當咱們在 Trie 樹中進行查找的時候,就能夠拿字符的 ASCII 碼減去 'a' 的 ASCII 碼來獲取其子節點的指針。編程語言
#include <iostream> #include <cstring> using namespace std; class TrieNode { public: char data; bool is_ending_char; TrieNode *children[26]; TrieNode(char ch) { data = ch; is_ending_char = false; for (int i = 0; i < 26; i++) children[i] = NULL; } }; class Trie { private: TrieNode *root; public: // 構造函數,根節點存儲無心義字符 '/' Trie() { root = new TrieNode('/'); } // 向 Trie 樹中添加一個字符串 void insert_string(const char str[]) { TrieNode *cur = root; for (unsigned int i = 0; i < strlen(str); i++) { int index = int(str[i] - 'a'); if (cur->children[index] == NULL) { TrieNode *temp = new TrieNode(str[i]); cur->children[index] = temp; } cur = cur->children[index]; } cur->is_ending_char = true; } // 在 Trie 樹中查找一個字符串 bool search_string(const char str[]) { TrieNode *cur = root; for (unsigned int i = 0; i < strlen(str); i++) { int index = int(str[i] - 'a'); if (cur->children[index] == NULL) { return false; } cur = cur->children[index]; } if (cur->is_ending_char == true) return true; else return false; } }; int main() { char str[][8] = {"how", "hi", "her", "hello", "so", "see", "however"}; Trie test; for (int i = 0; i < 7; i++) { test.insert_string(str[i]); } cout << "Finding \'her\': " << test.search_string("her") << endl; cout << "Finding \'he\': " << test.search_string("he") << endl; cout << "Finding \'how\': " << test.search_string("how") << endl; cout << "Finding \'however\': " << test.search_string("however") << endl; return 0; }
在構建 Trie 樹的過程當中,須要掃描全部的字符串,時間複雜度爲 O(n),其中 n 表示全部字符串的長度之和。而在 Trie 樹中進行查找的話,若是待查找字符串的長度爲 k 的話,那最多隻須要對比 k 個節點便可,時間複雜度爲 O(k)。編輯器
在上面的例子中,Trie 樹的每一個節點都要存儲 26 個指針,儘管某些節點的子節點不多,咱們依然要維護這麼一個長度的數組。另外,若是字符串中不只包含小寫字母,並且包含大寫字母、數字甚至是中文等,那就會須要更多的存儲空間。也就是說,在某些狀況下,Trie 樹並不必定會節省內存空間,尤爲是在重複前綴很少的時候。函數
固然,儘管 Trie 樹可能會很浪費內存,可是確實很是高效,這也是一種空間換時間的折中。若是咱們能夠稍微犧牲一點查詢的效率,那就能夠選用數組、散列表、紅黑樹等其餘數據結構來存儲一個節點的子節點指針。
假設咱們使用數組,數組中的指針按照所指向子節點的字符大小順序排列。這樣,在查找的時候,咱們能夠經過二分算法來快速找到指向子節點的指針。可是,在往 Trie 樹中插入字符串的話,爲了維護數組的有序性,就會稍微慢了點。
另外,還能夠採用縮點優化,將只有一個子節點並且不是結束節點的節點與其子節點進行合併,來節省空間,但這也增長了編碼難度。
在字符串匹配或者說查找問題上,Trie 樹對要處理的字符串有極其嚴格的要求。
所以,在工程中,咱們更傾向於使用散列表或者紅黑樹,它們都不須要本身去實現,直接利用編程語言中提供的線程類庫就行。實際上,Trie 樹不適合這種精確查找,更適合的是查找前綴匹配的字符串,也就是搜索時的關鍵詞提示功能。
假設關鍵詞庫由用戶的熱門搜索關鍵詞組成,咱們將這個詞庫構建成一個 Trie 樹。當用戶輸入其中某個單詞的時候,把這個詞做爲一個前綴子串在 Trie 樹中匹配。還以上面爲例,當用戶輸入 'h' 時,咱們就能夠將以 'h' 爲前綴的單詞 hello,her,hi,how 展現在搜索提示框,當用戶輸入 'he' 時,咱們就能夠將以 'h' 爲前綴的單詞 hello,her 展現在搜索提示框。這就是搜索關鍵詞提示的最基本的算法原理。
另外,Trie 樹還能夠擴展到更加普遍的應用上,好比輸入法、代碼編輯器和瀏覽器的自動輸入補全功能。
獲取更多精彩,請關注「seniusen」!