原文: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个子节点。

 #region Trie树节点
/// <summary>
/// Trie树节点
/// </summary>
public class TrieNode
{
/// <summary>
/// 26个字符,也就是26叉树
/// </summary>
public TrieNode[] childNodes; /// <summary>
/// 词频统计
/// </summary>
public int freq; /// <summary>
/// 记录该节点的字符
/// </summary>
public char nodeChar; /// <summary>
/// 插入记录时的编码id
/// </summary>
public HashSet<int> hashSet = new HashSet<int>(); /// <summary>
/// 初始化
/// </summary>
public TrieNode()
{
childNodes = new TrieNode[];
freq = ;
}
}
#endregion

2: 添加操作

既然是26叉树,那么当前节点的后续子节点是放在当前节点的哪一叉中,也就是放在childNodes中哪一个位置,这里我们采用

int k = word[0] - 'a'来计算位置。

         /// <summary>
/// 插入操作
/// </summary>
/// <param name="root"></param>
/// <param name="s"></param>
public void AddTrieNode(ref TrieNode root, string word, int id)
{
if (word.Length == )
return; //求字符地址,方便将该字符放入到26叉树中的哪一叉中
int k = word[] - 'a'; //如果该叉树为空,则初始化
if (root.childNodes[k] == null)
{
root.childNodes[k] = new TrieNode(); //记录下字符
root.childNodes[k].nodeChar = word[];
} //该id途径的节点
root.childNodes[k].hashSet.Add(id); var nextWord = word.Substring(); //说明是最后一个字符,统计该词出现的次数
if (nextWord.Length == )
root.childNodes[k].freq++; AddTrieNode(ref root.childNodes[k], nextWord, id);
}
#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 == )
return; //求字符地址,方便将该字符放入到26叉树种的哪一颗树中
int k = word[] - 'a'; //如果该叉树为空,则说明没有找到要删除的点
if (root.childNodes[k] == null)
return; var nextWord = word.Substring(); //如果是最后一个单词,则减去词频
if (word.Length == && root.childNodes[k].freq > )
root.childNodes[k].freq--; //删除途经节点
root.childNodes[k].hashSet.Remove(id); DeleteTrieNode(ref root.childNodes[k], nextWord, id);
}

4:测试

这里我从网上下载了一套的词汇表,共2279条词汇,现在我们要做的就是检索“go”开头的词汇,并统计go出现的频率。

        public static void Main()
{
Trie trie = new Trie(); var file = File.ReadAllLines(Environment.CurrentDirectory + "//1.txt"); foreach (var item in file)
{
var sp = item.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); trie.AddTrieNode(sp.LastOrDefault().ToLower(), Convert.ToInt32(sp[]));
} Stopwatch watch = Stopwatch.StartNew(); //检索go开头的字符串
var hashSet = trie.SearchTrie("go"); foreach (var item in hashSet)
{
Console.WriteLine("当前字符串的编号ID为:{0}", item);
} watch.Stop(); Console.WriteLine("耗费时间:{0}", watch.ElapsedMilliseconds); Console.WriteLine("\n\ngo 出现的次数为:{0}\n\n", trie.WordCount("go"));
}

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

测试文件:1.txt

完整代码:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.IO; namespace ConsoleApplication2
{
public class Program
{
public static void Main()
{
Trie trie = new Trie(); var file = File.ReadAllLines(Environment.CurrentDirectory + "//1.txt"); foreach (var item in file)
{
var sp = item.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); trie.AddTrieNode(sp.LastOrDefault().ToLower(), Convert.ToInt32(sp[]));
} Stopwatch watch = Stopwatch.StartNew(); //检索go开头的字符串
var hashSet = trie.SearchTrie("go"); foreach (var item in hashSet)
{
Console.WriteLine("当前字符串的编号ID为:{0}", item);
} watch.Stop(); Console.WriteLine("耗费时间:{0}", watch.ElapsedMilliseconds); Console.WriteLine("\n\ngo 出现的次数为:{0}\n\n", trie.WordCount("go"));
}
} public class Trie
{
public TrieNode trieNode = new TrieNode(); #region Trie树节点
/// <summary>
/// Trie树节点
/// </summary>
public class TrieNode
{
/// <summary>
/// 26个字符,也就是26叉树
/// </summary>
public TrieNode[] childNodes; /// <summary>
/// 词频统计
/// </summary>
public int freq; /// <summary>
/// 记录该节点的字符
/// </summary>
public char nodeChar; /// <summary>
/// 插入记录时的编号id
/// </summary>
public HashSet<int> hashSet = new HashSet<int>(); /// <summary>
/// 初始化
/// </summary>
public TrieNode()
{
childNodes = new TrieNode[];
freq = ;
}
}
#endregion #region 插入操作
/// <summary>
/// 插入操作
/// </summary>
/// <param name="word"></param>
/// <param name="id"></param>
public void AddTrieNode(string word, int id)
{
AddTrieNode(ref trieNode, word, id);
} /// <summary>
/// 插入操作
/// </summary>
/// <param name="root"></param>
/// <param name="s"></param>
public void AddTrieNode(ref TrieNode root, string word, int id)
{
if (word.Length == )
return; //求字符地址,方便将该字符放入到26叉树中的哪一叉中
int k = word[] - 'a'; //如果该叉树为空,则初始化
if (root.childNodes[k] == null)
{
root.childNodes[k] = new TrieNode(); //记录下字符
root.childNodes[k].nodeChar = word[];
} //该id途径的节点
root.childNodes[k].hashSet.Add(id); var nextWord = word.Substring(); //说明是最后一个字符,统计该词出现的次数
if (nextWord.Length == )
root.childNodes[k].freq++; AddTrieNode(ref root.childNodes[k], nextWord, id);
}
#endregion #region 检索操作
/// <summary>
/// 检索单词的前缀,返回改前缀的Hash集合
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public HashSet<int> SearchTrie(string s)
{
HashSet<int> hashSet = new HashSet<int>(); return SearchTrie(ref trieNode, s, ref hashSet);
} /// <summary>
/// 检索单词的前缀,返回改前缀的Hash集合
/// </summary>
/// <param name="root"></param>
/// <param name="s"></param>
/// <returns></returns>
public HashSet<int> SearchTrie(ref TrieNode root, string word, ref HashSet<int> hashSet)
{
if (word.Length == )
return hashSet; int k = word[] - 'a'; var nextWord = word.Substring(); if (nextWord.Length == )
{
//采用动态规划的思想,word最后节点记录这途经的id
hashSet = root.childNodes[k].hashSet;
} SearchTrie(ref root.childNodes[k], nextWord, ref hashSet); return hashSet;
}
#endregion #region 统计指定单词出现的次数 /// <summary>
/// 统计指定单词出现的次数
/// </summary>
/// <param name="root"></param>
/// <param name="word"></param>
/// <returns></returns>
public int WordCount(string word)
{
int count = ; WordCount(ref trieNode, word, ref count); return count;
} /// <summary>
/// 统计指定单词出现的次数
/// </summary>
/// <param name="root"></param>
/// <param name="word"></param>
/// <param name="hashSet"></param>
/// <returns></returns>
public void WordCount(ref TrieNode root, string word, ref int count)
{
if (word.Length == )
return; int k = word[] - 'a'; var nextWord = word.Substring(); if (nextWord.Length == )
{
//采用动态规划的思想,word最后节点记录这途经的id
count = root.childNodes[k].freq;
} WordCount(ref root.childNodes[k], nextWord, ref count);
} #endregion #region 修改操作
/// <summary>
/// 修改操作
/// </summary>
/// <param name="newWord"></param>
/// <param name="oldWord"></param>
/// <param name="id"></param>
public void UpdateTrieNode(string newWord, string oldWord, int id)
{
UpdateTrieNode(ref trieNode, newWord, oldWord, id);
} /// <summary>
/// 修改操作
/// </summary>
/// <param name="root"></param>
/// <param name="newWord"></param>
/// <param name="oldWord"></param>
/// <param name="id"></param>
public void UpdateTrieNode(ref TrieNode root, string newWord, string oldWord, int id)
{
//先删除
DeleteTrieNode(oldWord, id); //再添加
AddTrieNode(newWord, id);
}
#endregion #region 删除操作
/// <summary>
/// 删除操作
/// </summary>
/// <param name="root"></param>
/// <param name="newWord"></param>
/// <param name="oldWord"></param>
/// <param name="id"></param>
public void DeleteTrieNode(string word, int id)
{
DeleteTrieNode(ref trieNode, word, id);
} /// <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 == )
return; //求字符地址,方便将该字符放入到26叉树种的哪一颗树中
int k = word[] - 'a'; //如果该叉树为空,则说明没有找到要删除的点
if (root.childNodes[k] == null)
return; var nextWord = word.Substring(); //如果是最后一个单词,则减去词频
if (word.Length == && root.childNodes[k].freq > )
root.childNodes[k].freq--; //删除途经节点
root.childNodes[k].hashSet.Remove(id); DeleteTrieNode(ref root.childNodes[k], nextWord, id);
}
#endregion
}
}

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

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

    原文:6天通吃树结构-- 第三天 Treap树 我们知道,二叉查找树相对来说比较容易形成最坏的链表情况,所以前辈们想尽了各种优化策略,包括AVL,红黑,以及今天 要讲的Treap树. Treap树算是 ...

  2. [算法]Trie树

    我是好文章的搬运工,原文来自博客园,博主一线码农,选自”6天通吃树结构“系列,地址:http://www.cnblogs.com/huangxincheng/archive/2012/11/25/27 ...

  3. 字典树(Trie树)的实现及应用

    >>字典树的概念 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树.与二叉查找树不同,Trie树的 ...

  4. Trie树(字典树)(1)

    Trie树.又称字典树,单词查找树或者前缀树,是一种用于高速检索的多叉树结构. Trie树与二叉搜索树不同,键不是直接保存在节点中,而是由节点在树中的位置决定. 一个节点的全部子孙都有同样的前缀(pr ...

  5. [TJOI2018] Xor 异或 (可持久化Trie,树链剖分)

    题目描述 现在有一颗以 1 为根节点的由 n 个节点组成的树,树上每个节点上都有一个权值 \(v_i\).现在有 Q 次操作,操作如下: 1 x y :查询节点 x 的子树中与 y 异或结果的最大值. ...

  6. Atitit 常见的树形结构 红黑树  二叉树   B树 B+树  Trie树 attilax理解与总结

    Atitit 常见的树形结构 红黑树  二叉树   B树 B+树  Trie树 attilax理解与总结 1.1. 树形结构-- 一对多的关系1 1.2. 树的相关术语: 1 1.3. 常见的树形结构 ...

  7. 浅谈树形结构的特性和应用(上):多叉树,红黑树,堆,Trie树,B树,B+树...

    上篇文章我们主要介绍了线性数据结构,本篇233酱带大家康康 无所不在的非线性数据结构之一:树形结构的特点和应用. 树形结构,是指:数据元素之间的关系像一颗树的数据结构.我们看图说话: 它具有以下特点: ...

  8. hbase源码系列(五)Trie单词查找树

    在上一章中提到了编码压缩,讲了一个简单的DataBlockEncoding.PREFIX算法,它用的是前序编码压缩的算法,它搜索到时候,是全扫描的方式搜索的,如此一来,搜索效率实在是不敢恭维,所以在h ...

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

    #1014 : Trie树 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助, ...

随机推荐

  1. MyEclipse每次修改js有另一种部署

    今天在做时间格式显示这块内容的时候.我改动了这个js函数,同一时候凝视的掉了其它的一些替换的方法,可是在浏览器中执行发现效果一点也没有改变.于是debug js,发现浏览器中识别的还是之前未改动的js ...

  2. 在C#主线程和子线程将数据传递给对方如何实现

    在C#中主线程和子线程怎样实现互相传递数据 老帅 在C#中创建线程Thread时,能够有多种方法,而主线程和子线程之间又怎样实现互相传递数据,每种创建方法传递參数的效果是不同的,逐一看一下:  一.不 ...

  3. lintcode 1: Data Stream Median

    Data Stream Median Numbers keep coming, return the median of numbers at every time a new number adde ...

  4. ECharts SSH+JQueryAjax+Json+JSP在数据库中的数据来填充ECharts在

    1导入包.设定SSH框架. 进口JQuery的JS包.<script src="JS/jquery-1.7.1.js"></script> 导入EChart ...

  5. OpenCV——Delaunay三角 [转载]

    从这个博客转载 http://blog.csdn.net/raby_gyl/article/details/17409717 请其它同学转载时注明原始文章的出处! Delaunay三角剖分是1934年 ...

  6. Tomcat通过配置一个虚拟路径管理web工程

    关于虚拟路径.学问javaweb训练课程,如今,鉴于这种情况下老师. 当我们的项目,当在不同的文件夹项目.我们如何使用tomcat去管理web工程. 教师提出的解决方案是 使用虚拟路径方式,并按照实施 ...

  7. MFC9.0 Outlook控件的标题显示无法改动

    这是我在开发中遇到的问题,现记录下来,以便帮助你们. 不想看废话的能够仅仅看最后三行,但你会错过非常多. 俗话说的好啊,"Wise men learn by other men's mist ...

  8. C#中的动态特性

    众所周知,C#和Java一样,都是一门静态语言.在C# 4.0之前,想要和动态语言(诸如Python.Javascript等)进行方便地互操作是一件不太容易的事情.而C# 4.0为我们带来的dynam ...

  9. 移动端 transition动画函数的封装(仿Zepto)以及 requestAnimationFrame动画函数封装(仿jQuery)

    移动端 css3 transition 动画 ,requestAnimationFrame 动画  对于性能的要求,h5优先考虑: 移动端 单页有时候 制作只用到简单的css3动画即可,我们封装一下, ...

  10. LeetCode——Climbing Stairs

    You are climbing a stair case. It takes n steps to reach to the top. Each time you can either climb ...