6天通吃树结构—— 第五天 Trie树

原文: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

完整代码:

  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 }
时间: 2024-12-20 14:58:28

6天通吃树结构—— 第五天 Trie树的相关文章

6天通吃树结构—— 第三天 Treap树

原文:6天通吃树结构-- 第三天 Treap树 我们知道,二叉查找树相对来说比较容易形成最坏的链表情况,所以前辈们想尽了各种优化策略,包括AVL,红黑,以及今天 要讲的Treap树. Treap树算是一种简单的优化策略,这名字大家也能猜到,树和堆的合体,其实原理比较简单,在树中维护一个"优先级“,”优先级“ 采用随机数的方法,但是”优先级“必须满足根堆的性质,当然是“大根堆”或者“小根堆”都无所谓,比如下面的一棵树: 从树中我们可以看到: ①:节点中的key满足“二叉查找树”. ②:节点中的“优

6天通吃树结构—— 第四天 伸展树

原文:6天通吃树结构-- 第四天 伸展树 我们知道AVL树为了保持严格的平衡,所以在数据插入上会呈现过多的旋转,影响了插入和删除的性能,此时AVL的一个变种 伸展树(Splay)就应运而生了,我们知道万事万物都遵循一个“八二原则“,也就是说80%的人只会用到20%的数据,比如说我们 的“QQ输入法”,平常打的字也就那么多,或许还没有20%呢. 一:伸展树 1:思想 伸展树的原理就是这样的一个”八二原则”,比如我要查询树中的“节点7”,如果我们是AVL的思路,每次都查询“节点7”,那么当这 棵树中

6天通吃树结构—— 第一天 二叉查找树

原文:6天通吃树结构-- 第一天 二叉查找树 一直很想写一个关于树结构的专题,再一个就是很多初级点的码农会认为树结构无用论,其实归根到底还是不清楚树的实际用途. 一:场景: 1:现状 前几天我的一个大学同学负责的网站出现了严重的性能瓶颈,由于业务是写入和读取都是密集型,如果做缓存,时间间隔也只能在30s左 右,否则就会引起客户纠纷,所以同学也就没有做缓存,通过测试发现慢就慢在数据读取上面,总共需要10s,天啊...原来首页的加载关联 到了4张表,而且表数据中最多的在10w条以上,可以想象4张巨大

6天通吃树结构—— 第二天 平衡二叉树

原文:6天通吃树结构-- 第二天 平衡二叉树 上一篇我们聊过,二叉查找树不是严格的O(logN),导致了在真实场景中没有用武之地,谁也不愿意有O(N)的情况发生, 作为一名码农,肯定会希望能把“范围查找”做到地球人都不能优化的地步. 当有很多数据灌到我的树中时,我肯定会希望最好是以“完全二叉树”的形式展现,这样我才能做到“查找”是严格的O(logN), 比如把这种”树“调正到如下结构. 这里就涉及到了“树节点”的旋转,也是我们今天要聊到的内容. 一:平衡二叉树(AVL) 1:定义 父节点的左子树

字典树Trie树

一.字典树 字典树--Trie树,又称为前缀树(Prefix Tree).单词查找树或键树,是一种多叉树结构. 上图是一棵Trie树,表示了关键字集合{"a", "to", "tea", "ted", "ten", "i", "in", "inn"} .从上图可以归纳出Trie树的基本性质: 1. 根节点不包含字符,除根节点外的每一个子节点都包含一

Trie树(字典树)(1)

Trie树.又称字典树,单词查找树或者前缀树,是一种用于高速检索的多叉树结构. Trie树与二叉搜索树不同,键不是直接保存在节点中,而是由节点在树中的位置决定. 一个节点的全部子孙都有同样的前缀(prefix),也就是这个节点相应的字符串,而根节点相应空字符串.普通情况下.不是全部的节点都有相应的值,仅仅有叶子节点和部分内部节点所相应的键才有相关的值. A trie, pronounced "try", is a tree that exploits some structure in

Atitit 常见的树形结构 红黑树 &#160;二叉树 &#160;&#160;B树 B+树 &#160;Trie树&#160;attilax理解与总结

Atitit 常见的树形结构 红黑树  二叉树   B树 B+树  Trie树 attilax理解与总结 1.1. 树形结构-- 一对多的关系1 1.2. 树的相关术语: 1 1.3. 常见的树形结构 红黑树  二叉树   B树 B+树  Trie树2 1.4. 满二叉树和完全二叉树..完全二叉树说明深度达到完全了.2 1.5. 属的逻辑表示 树形比奥死,括号表示,文氏图,凹镜法表示3 1.6. 二叉树是数据结构中一种重要的数据结构,也是树表家族最为基础的结构.3 1.6.1. 3.2 平衡二叉

Hihocoder #1014 : Trie树 (字典数树统计前缀的出现次数 *【模板】 基于指针结构体实现 )

#1014 : Trie树 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编程的学习道路上一同前进. 这一天,他们遇到了一本词典,于是小Hi就向小Ho提出了那个经典的问题:“小Ho,你能不能对于每一个我给出的字符串,都在这个词典里面找到以这个字符串开头的所有单词呢?” 身经百战的小Ho答道:“怎么会不能呢!你每给我一个字符串,我就依次遍历词典里的所有单词,检查你给我的字

跳跃表,字典树(单词查找树,Trie树),后缀树,KMP算法,AC 自动机相关算法原理详细汇总

第一部分:跳跃表 本文将总结一种数据结构:跳跃表.前半部分跳跃表性质和操作的介绍直接摘自<让算法的效率跳起来--浅谈"跳跃表"的相关操作及其应用>上海市华东师范大学第二附属中学 魏冉.之后将附上跳跃表的源代码,以及本人对其的了解.难免有错误之处,希望指正,共同进步.谢谢. 跳跃表(Skip List)是1987年才诞生的一种崭新的数据结构,它在进行查找.插入.删除等操作时的期望时间复杂度均为O(logn),有着近乎替代平衡树的本领.而且最重要的一点,就是它的编程复杂度较同类