6天通吃樹結構—— 第五天 Trie樹


     很有段時間沒寫此系列了,今天我們來說Trie樹,Trie樹的名字有很多,比如字典樹,前綴樹等等。

一:概念

     下面我們有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         }
36         #endregion

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         }
34         #endregion

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);
        }

4:測試

   這里我從網上下載了一套的詞匯表,共2279條詞匯,現在我們要做的就是檢索“go”開頭的詞匯,並統計go出現的頻率。

 1        public static void Main()
 2         {
 3             Trie trie = new Trie();
 4 
 5             var file = File.ReadAllLines(Environment.CurrentDirectory + "//1.txt");
 6 
 7             foreach (var item in file)
 8             {
 9                 var sp = item.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
10 
11                 trie.AddTrieNode(sp.LastOrDefault().ToLower(), Convert.ToInt32(sp[0]));
12             }
13 
14             Stopwatch watch = Stopwatch.StartNew();
15 
16             //檢索go開頭的字符串
17             var hashSet = trie.SearchTrie("go");
18 
19             foreach (var item in hashSet)
20             {
21                 Console.WriteLine("當前字符串的編號ID為:{0}", item);
22             }
23 
24             watch.Stop();
25 
26             Console.WriteLine("耗費時間:{0}", watch.ElapsedMilliseconds);
27 
28             Console.WriteLine("\n\ngo 出現的次數為:{0}\n\n", trie.WordCount("go"));
29         }

下面我們拿着ID到txt中去找一找,嘿嘿,是不是很有意思。

測試文件:1.txt

完整代碼:

View Code
  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Diagnostics;
  6 using System.Threading;
  7 using System.IO;
  8 
  9 namespace ConsoleApplication2
 10 {
 11     public class Program
 12     {
 13         public static void Main()
 14         {
 15             Trie trie = new Trie();
 16 
 17             var file = File.ReadAllLines(Environment.CurrentDirectory + "//1.txt");
 18 
 19             foreach (var item in file)
 20             {
 21                 var sp = item.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
 22 
 23                 trie.AddTrieNode(sp.LastOrDefault().ToLower(), Convert.ToInt32(sp[0]));
 24             }
 25 
 26             Stopwatch watch = Stopwatch.StartNew();
 27 
 28             //檢索go開頭的字符串
 29             var hashSet = trie.SearchTrie("go");
 30 
 31             foreach (var item in hashSet)
 32             {
 33                 Console.WriteLine("當前字符串的編號ID為:{0}", item);
 34             }
 35 
 36             watch.Stop();
 37 
 38             Console.WriteLine("耗費時間:{0}", watch.ElapsedMilliseconds);
 39 
 40             Console.WriteLine("\n\ngo 出現的次數為:{0}\n\n", trie.WordCount("go"));
 41         }
 42     }
 43 
 44     public class Trie
 45     {
 46         public TrieNode trieNode = new TrieNode();
 47 
 48         #region Trie樹節點
 49         /// <summary>
 50         /// Trie樹節點
 51         /// </summary>
 52         public class TrieNode
 53         {
 54             /// <summary>
 55             /// 26個字符,也就是26叉樹
 56             /// </summary>
 57             public TrieNode[] childNodes;
 58 
 59             /// <summary>
 60             /// 詞頻統計
 61             /// </summary>
 62             public int freq;
 63 
 64             /// <summary>
 65             /// 記錄該節點的字符
 66             /// </summary>
 67             public char nodeChar;
 68 
 69             /// <summary>
 70             /// 插入記錄時的編號id
 71             /// </summary>
 72             public HashSet<int> hashSet = new HashSet<int>();
 73 
 74             /// <summary>
 75             /// 初始化
 76             /// </summary>
 77             public TrieNode()
 78             {
 79                 childNodes = new TrieNode[26];
 80                 freq = 0;
 81             }
 82         }
 83         #endregion
 84 
 85         #region 插入操作
 86         /// <summary>
 87         /// 插入操作
 88         /// </summary>
 89         /// <param name="word"></param>
 90         /// <param name="id"></param>
 91         public void AddTrieNode(string word, int id)
 92         {
 93             AddTrieNode(ref trieNode, word, id);
 94         }
 95 
 96         /// <summary>
 97         /// 插入操作
 98         /// </summary>
 99         /// <param name="root"></param>
100         /// <param name="s"></param>
101         public void AddTrieNode(ref TrieNode root, string word, int id)
102         {
103             if (word.Length == 0)
104                 return;
105 
106             //求字符地址,方便將該字符放入到26叉樹中的哪一叉中
107             int k = word[0] - 'a';
108 
109             //如果該叉樹為空,則初始化
110             if (root.childNodes[k] == null)
111             {
112                 root.childNodes[k] = new TrieNode();
113 
114                 //記錄下字符
115                 root.childNodes[k].nodeChar = word[0];
116             }
117 
118             //該id途徑的節點
119             root.childNodes[k].hashSet.Add(id);
120 
121             var nextWord = word.Substring(1);
122 
123             //說明是最后一個字符,統計該詞出現的次數
124             if (nextWord.Length == 0)
125                 root.childNodes[k].freq++;
126 
127             AddTrieNode(ref root.childNodes[k], nextWord, id);
128         }
129         #endregion
130 
131         #region 檢索操作
132         /// <summary>
133         /// 檢索單詞的前綴,返回改前綴的Hash集合
134         /// </summary>
135         /// <param name="s"></param>
136         /// <returns></returns>
137         public HashSet<int> SearchTrie(string s)
138         {
139             HashSet<int> hashSet = new HashSet<int>();
140 
141             return SearchTrie(ref trieNode, s, ref hashSet);
142         }
143 
144         /// <summary>
145         /// 檢索單詞的前綴,返回改前綴的Hash集合
146         /// </summary>
147         /// <param name="root"></param>
148         /// <param name="s"></param>
149         /// <returns></returns>
150         public HashSet<int> SearchTrie(ref TrieNode root, string word, ref HashSet<int> hashSet)
151         {
152             if (word.Length == 0)
153                 return hashSet;
154 
155             int k = word[0] - 'a';
156 
157             var nextWord = word.Substring(1);
158 
159             if (nextWord.Length == 0)
160             {
161                 //采用動態規划的思想,word最后節點記錄這途經的id
162                 hashSet = root.childNodes[k].hashSet;
163             }
164 
165             SearchTrie(ref root.childNodes[k], nextWord, ref hashSet);
166 
167             return hashSet;
168         }
169         #endregion
170 
171         #region 統計指定單詞出現的次數
172 
173         /// <summary>
174         /// 統計指定單詞出現的次數
175         /// </summary>
176         /// <param name="root"></param>
177         /// <param name="word"></param>
178         /// <returns></returns>
179         public int WordCount(string word)
180         {
181             int count = 0;
182 
183             WordCount(ref trieNode, word, ref count);
184 
185             return count;
186         }
187 
188         /// <summary>
189         /// 統計指定單詞出現的次數
190         /// </summary>
191         /// <param name="root"></param>
192         /// <param name="word"></param>
193         /// <param name="hashSet"></param>
194         /// <returns></returns>
195         public void WordCount(ref TrieNode root, string word, ref int count)
196         {
197             if (word.Length == 0)
198                 return;
199 
200             int k = word[0] - 'a';
201 
202             var nextWord = word.Substring(1);
203 
204             if (nextWord.Length == 0)
205             {
206                 //采用動態規划的思想,word最后節點記錄這途經的id
207                 count = root.childNodes[k].freq;
208             }
209 
210             WordCount(ref root.childNodes[k], nextWord, ref count);
211         }
212 
213         #endregion
214 
215         #region 修改操作
216         /// <summary>
217         /// 修改操作
218         /// </summary>
219         /// <param name="newWord"></param>
220         /// <param name="oldWord"></param>
221         /// <param name="id"></param>
222         public void UpdateTrieNode(string newWord, string oldWord, int id)
223         {
224             UpdateTrieNode(ref trieNode, newWord, oldWord, id);
225         }
226 
227         /// <summary>
228         /// 修改操作
229         /// </summary>
230         /// <param name="root"></param>
231         /// <param name="newWord"></param>
232         /// <param name="oldWord"></param>
233         /// <param name="id"></param>
234         public void UpdateTrieNode(ref TrieNode root, string newWord, string oldWord, int id)
235         {
236             //先刪除
237             DeleteTrieNode(oldWord, id);
238 
239             //再添加
240             AddTrieNode(newWord, id);
241         }
242         #endregion
243 
244         #region 刪除操作
245         /// <summary>
246         ///  刪除操作
247         /// </summary>
248         /// <param name="root"></param>
249         /// <param name="newWord"></param>
250         /// <param name="oldWord"></param>
251         /// <param name="id"></param>
252         public void DeleteTrieNode(string word, int id)
253         {
254             DeleteTrieNode(ref trieNode, word, id);
255         }
256 
257         /// <summary>
258         /// 刪除操作
259         /// </summary>
260         /// <param name="root"></param>
261         /// <param name="newWord"></param>
262         /// <param name="oldWord"></param>
263         /// <param name="id"></param>
264         public void DeleteTrieNode(ref TrieNode root, string word, int id)
265         {
266             if (word.Length == 0)
267                 return;
268 
269             //求字符地址,方便將該字符放入到26叉樹種的哪一顆樹中
270             int k = word[0] - 'a';
271 
272             //如果該叉樹為空,則說明沒有找到要刪除的點
273             if (root.childNodes[k] == null)
274                 return;
275 
276             var nextWord = word.Substring(1);
277 
278             //如果是最后一個單詞,則減去詞頻
279             if (word.Length == 0 && root.childNodes[k].freq > 0)
280                 root.childNodes[k].freq--;
281 
282             //刪除途經節點
283             root.childNodes[k].hashSet.Remove(id);
284 
285             DeleteTrieNode(ref root.childNodes[k], nextWord, id);
286         }
287         #endregion
288     }
289 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM