1.字典樹
曾經遇到這樣一個問題:很多單詞,這些單詞只含小寫字母,並且不會有重復的單詞出現,現在要統計出以某個字符串為前綴的單詞數量,單詞本身也是自己的前綴。先看看用常規的方法解決這個問題的復雜度。假設單詞表容量為M,需要統計的前綴數量為N,前綴的平均長度是L,則常規算法思路是:對於每個前綴搜索每個單詞,看看這個前綴是不是這個單詞的前綴,如果是數量+1。這樣的話時間復雜度為O(N*M*L),如果N相當大的話,這個算法的復雜度將無法接受啦。其實這就是字典樹的典型應用啦。
我們先學習一下字典樹,在解決上面的問題。字典樹又稱trie樹,從名字上看很顯然是一種樹形結構了。字典樹有以下幾個特點:1.利用串的公共前綴,節約內存;2.根結點不包含任何字母;3.其余結點僅包含一個字母;4.每個結點的子節點包含字母不同。看看一個例子吧:下面就是一個字典樹(圖片來自百度百科)
上面的樹就是一顆典型的字典樹了,字典樹中存儲的單詞包括:abc、abcd、abd、b、bcd、efg、hig,即:所有標記為紅心的才是單詞的結尾字母。對比上面的trie樹的特點仔細看一下,理解一下到底什么是字典樹。實際上字典樹包括常見的兩種種操作是:查找和插入操作。我覺得有必要看看字典樹的基礎代碼:(代碼)

1 //字典樹的每個節點 2 struct node 3 { 4 bool isWord;//判斷當前字母是否是單詞最后一個字母的標志 5 node *next[26];//后繼結點可能是26個小寫字母 6 node()//構造函數 7 { 8 isWord = false; 9 for(int i = 0 ; i < 26 ; i++) 10 { 11 next[i] = NULL; 12 } 13 } 14 }; 15 //字典樹 16 class TrieTree 17 { 18 public: 19 node *root; 20 TrieTree() 21 { 22 root = NULL; 23 } 24 //向字典樹中插入字符串str 25 void Insert(string str) 26 { 27 if(!root)//判斷根節點是否為空 28 root = new node; 29 node *location = root; 30 for(int i = 0 ; i < str.length() ; i++) 31 { 32 int num = str[i] - 'a'; 33 //獲得當前str[i]具體是哪個小寫字母,並指導應該存入哪個子節點, 34 //在字典樹中,字母b永遠只可能屬於第二個子節點,同樣的道理,字母d永遠的屬於第四個節點 35 //應該注意的是:這樣的做法浪費了大量的空節點空間。 36 if(location->next[num] == NULL) 37 { 38 location->next[num] = new node; 39 } 40 location = location->next[num]; 41 } 42 location->isWord = true;//插入后當前節點應該是一個字符串的最后一個節點 43 } 44 //在字典樹中查找字符串str 45 bool Search(string str) 46 { 47 node *location = root; 48 for(int i=0;i<str.length();i++) 49 { 50 int num = str[i] - 'a'; 51 if(location->next[num] == NULL) 52 return false; 53 location = location->next[num]; 54 } 55 //雖然str中的所有字符全在字典樹的某個路徑上,但是只有isword = true時才是真正的單詞 56 return location->isWord; 57 } 58 };
小結:看過上面的代碼,是否發現這個代碼有什么問題呢??即盡管這個實現方式查找的效率很高,時間復雜度是O(m),m是要查找的單詞中包含的字母的個數。但是確浪費大量存放空指針的存儲空間。因為不可能每個節點的子節點都包含26個字母的。話又說回來,另外一個方面公共前綴只存儲了一次,有減少了存儲空間。所以對於這個問題,應該個人覺得應該這樣想,字典樹存在的意義是解決快速搜索的問題,所以采取以空間換時間的作法也毋庸置疑。
現在字典樹的知識學完了,是時候解決一下文章開頭提到的問題了。我們的解決思路是:把給出的所有單詞建立一個字典樹,仿照上面的的代碼,只需要在客戶端每次調用插入單詞的操作,就可以建立一個字典樹了。但是本題要解決的問題是,統計出以某個字符串為前綴的單詞總數,想一想怎么改動一下呢?實際上只需要在struct node中做一些改動,改動如下:

1 struct Trienode 2 { 3 int count; 4 bool isWord;//判斷當前字母是否是單詞最后一個字母的標志 5 node *next[26];//后繼結點可能是26個小寫字母 6 node()//構造函數 7 { 8 count = 1; 9 isWord = false; 10 for(int i = 0 ; i < 26 ; i++) 11 { 12 next[i] = NULL; 13 } 14 } 15 };
看出來了吧,加入了一個count成員,用來記錄每個節點在單詞中出現的次數,然后在對應的插入操作(Insert)中,每次插入時經過的節點作count++操作;這樣進行搜索(Search)時,若搜索成功只需要返回搜索終止節點的count變量的值,就是以某個字符串為前綴的單詞總數啦。現在不妨來分析一下算法的時間復雜度,每次查找的時間復雜度是O(L),L是要搜索的前綴平均長度。現在有N個前綴需要處理,那么整個時間復雜度就是O(N*L),然后還要加上建字典樹的時間復雜度,單詞數為M,不妨設單詞的平均長度是L',那么建字典樹的時間復雜度就是O(M*L'),所以整個算法的時間復雜度是O(N*L + M*L'),想必之前的復雜度O(N*M*L),這是個不小的改進。當然我們也說了,這引入一些輔助空間,加快了搜索的速度。實際應用中M往往是大量的,比如搜索引擎中的字典,要在百度中進行搜索,那么采用字典樹的地位就舉足輕重啦。采用字典樹可以大大的降低搜索的復雜度。
2.字典樹的應用和好處
那么字典樹到底有哪些典型的應用呢?
1.字典樹在串的快速檢索中的應用。
給出N個單詞組成的熟詞表,以及一篇全用小寫英文書寫的文章,請你按最早出現的順序寫出所有不在熟詞表中的生詞。在這道題中,我們可以用字典樹,先把熟詞建一棵樹,然后讀入文章進行比較,這種方法效率是比較高的。
2. 字典樹在“串”排序方面的應用
給定N個互不相同的僅由一個單詞構成的英文名,讓你將他們按字典序從小到大輸出用字典樹進行排序,采用數組的方式創建字典樹,這棵樹的每個結點的所有兒子很顯然地按照其字母大小排序。對這棵樹進行先序遍歷即可。
3. 字典樹在最長公共前綴問題的應用
對所有串建立字典樹,對於兩個串的最長公共前綴的長度即他們所在的結點的公共祖先個數,於是,問題就轉化為最近公共祖先問題。
使用字典樹的好處:
1.利用字符串的公共前綴來節約存儲空間。
2.最大限度地減少無謂的字符串比較,查詢效率比較高。例如:若要查找的字符長度是5,而總共有單詞的數目是26^5=11881376,利用trie樹,利用5次比較可以從11881376個可能的關鍵字中檢索出指定的關鍵字,而利用二叉查找樹時間復雜度是O( log2n ),所以至少要進行log211881376=23.5次比較。可以看出來利用字典樹進行查找速度是比較快的。
學習中的一點總結,歡迎拍磚哦^^