原文: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. 移动web:tab选项卡

    平常做移动端会用到tab选项卡,这和PC端有些区别,移动端是触摸滑动切换,PC端是点击.移入切换. 这里滑动切换就是一个移动端事件的应用,这里主要用到的触摸事件:touchstart.touchmov ...

  2. WebAPI通过multipart/form-data方式同时上传文件以及数据(含HttpClient上传Demo)

    简单的Demo,用于了解WebAPI如何同时接收文件及数据,同时提供HttpClient模拟如何同时上传文件和数据的Demo,下面是HttpClient上传的Demo界面 1.HttpClient部分 ...

  3. ThinkPad E530 Fedora 20 无线上网问题

    它一直在使用 Fedora 家庭 Linux. 但它自带的无线网卡驱动似下一些问题,通常,有时连接,有时你不能. 经常搜索不到的家用无线路由器. 因为家里有网线所以一直没有在意.没什么事就折腾了一下. ...

  4. 如何在 Swift 中优雅地处理 JSON

    阅读目录 在Swift中使用JSON的问题 开始 基础用法 枚举(Enumeration) 下标(Subscripts) 打印 调试与错误处理 后记   因为Swift对于类型有非常严格的控制,它在处 ...

  5. 开源Math.NET基础数学类库使用(03)C#解析Matlab的mat格式

    原文:[原创]开源Math.NET基础数学类库使用(03)C#解析Matlab的mat格式 开源Math.NET基础数学类库使用系列文章总目录:   1.开源.NET基础数学计算组件Math.NET( ...

  6. 什么时候PHP经验MySQL存储过程

    1.MySQL存储过程 数据库语言,我们经常使用的操作SQL语句必须首先编译在运行时.然后运行,存储过程(Stored Procedure)它被设置为完成一个特定的功能SQL报表设置.编译存储在数据库 ...

  7. Oracle之Check约束实例具体解释

    Oracle | PL/SQL Check约束使用方法具体解释 1. 目标 实例解说在Oracle中怎样使用CHECK约束(创建.启用.禁用和删除) 2. 什么是Check约束? CHECK约束指在表 ...

  8. POJ3467(预处理)

    Cross Counting Time Limit: 1000MS   Memory Limit: 131072K Total Submissions: 1331   Accepted: 375 De ...

  9. hdu4888 Redraw Beautiful Drawings

    14更多学校的第二个问题 网络流量   分别以行,列作为结点建图 i行表示的结点到j列表示的结点的流量便是(i, j)的值 跑遍最大流   若满流了便是有解   推断是否unique  就是在残余网络 ...

  10. 容易centos配置docker维修

    首先.由于docker实施需要linux某些组件支持本身和内核特性.所以一定要确保centos版本号大于6,和内核版本号大于2.6.32-431.可轻松升级centos6到最新的版本号. sudo y ...