一:概念
下面我們有and,as,at,cn,com這些關鍵詞,那麼如何構建trie樹呢?
從上面的圖中,我們或多或少的可以發現一些好玩的特性。
第一:根節點不包含字符,除根節點外的每一個子節點都包含一個字符。
第二:從根節點到某一節點,路徑上經過的字符連接起來,就是該節點對應的字符串。
第三:每個單詞的公共前綴作爲一個字符節點保存。
二:使用範圍
既然學Trie樹,我們肯定要知道這玩意是用來幹嘛的。
第一:詞頻統計。
可能有人要說了,詞頻統計簡單啊,一個hash或者一個堆就可以打完收工,但問題來了,如果內存有限呢?還能這麼
玩嗎?所以這裏我們就可以用trie樹來壓縮下空間,因爲公共前綴都是用一個節點保存的。
第二: 前綴匹配
就拿上面的圖來說吧,如果我想獲取所有以"a"開頭的字符串,從圖中可以很明顯的看到是:and,as,at,如果不用trie樹,
你該怎麼做呢?很顯然樸素的做法時間複雜度爲O(N2) ,那麼用Trie樹就不一樣了,它可以做到h,h爲你檢索單詞的長度,
可以說這是秒殺的效果。
舉個例子:現有一個編號爲1的字符串”and“,我們要插入到trie樹中,採用動態規劃的思想,將編號”1“計入到每個途徑的節點中,
那麼以後我們要找”a“,”an“,”and"爲前綴的字符串的編號將會輕而易舉。
三:實際操作
到現在爲止,我想大家已經對trie樹有了大概的掌握,下面我們看看如何來實現。
1:定義trie樹節點
爲了方便,我也採用純英文字母,我們知道字母有26個,那麼我們構建的trie樹就是一個26叉樹,每個節點包含26個子節點。
1 #region Trie樹節點 2 /// <summary> 3 /// Trie樹節點 4 /// </summary> 5 public class TrieNode 6 { 7 /// <summary> 8 /// 26個字符,也就是26叉樹 9 /// </summary> 10 public TrieNode[] childNodes; 11 12 /// <summary> 13 /// 詞頻統計 14 /// </summary> 15 public int freq; 16 17 /// <summary> 18 /// 記錄該節點的字符 19 /// </summary> 20 public char nodeChar; 21 22 /// <summary> 23 /// 插入記錄時的編碼id 24 /// </summary> 25 public HashSet<int> hashSet = new HashSet<int>(); 26 27 /// <summary> 28 /// 初始化 29 /// </summary> 30 public TrieNode() 31 { 32 childNodes = new TrieNode[26]; 33 freq = 0; 34 } 35 }
2: 添加操作
既然是26叉樹,那麼當前節點的後續子節點是放在當前節點的哪一叉中,也就是放在childNodes中哪一個位置,這裏我們採用
int k = word[0] - 'a'來計算位置。
1 /// <summary> 2 /// 插入操作 3 /// </summary> 4 /// <param name="root"></param> 5 /// <param name="s"></param> 6 public void AddTrieNode(ref TrieNode root, string word, int id) 7 { 8 if (word.Length == 0) 9 return; 10 11 //求字符地址,方便將該字符放入到26叉樹中的哪一叉中 12 int k = word[0] - 'a'; 13 14 //如果該叉樹爲空,則初始化 15 if (root.childNodes[k] == null) 16 { 17 root.childNodes[k] = new TrieNode(); 18 19 //記錄下字符 20 root.childNodes[k].nodeChar = word[0]; 21 } 22 23 //該id途徑的節點 24 root.childNodes[k].hashSet.Add(id); 25 26 var nextWord = word.Substring(1); 27 28 //說明是最後一個字符,統計該詞出現的次數 29 if (nextWord.Length == 0) 30 root.childNodes[k].freq++; 31 32 AddTrieNode(ref root.childNodes[k], nextWord, id); 33 }
3:刪除操作
刪除操作中,我們不僅要刪除該節點的字符串編號,還要對詞頻減一操作。
/// <summary> /// 刪除操作 /// </summary> /// <param name="root"></param> /// <param name="newWord"></param> /// <param name="oldWord"></param> /// <param name="id"></param> public void DeleteTrieNode(ref TrieNode root, string word, int id) { if (word.Length == 0) return; //求字符地址,方便將該字符放入到26叉樹種的哪一顆樹中 int k = word[0] - 'a'; //如果該叉樹爲空,則說明沒有找到要刪除的點 if (root.childNodes[k] == null) return; var nextWord = word.Substring(1); //如果是最後一個單詞,則減去詞頻 if (word.Length == 0 && root.childNodes[k].freq > 0) root.childNodes[k].freq--; //刪除途經節點 root.childNodes[k].hashSet.Remove(id); DeleteTrieNode(ref root.childNodes[k], nextWord, id); }