[算法]Trie树
我是好文章的搬运工,原文来自博客园,博主一线码农,选自”6天通吃树结构“系列,地址:http://www.cnblogs.com/huangxincheng/archive/2012/11/25/2788268.html
一:概念
下面我们有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
完整代码:
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
}
}
[算法]Trie树的更多相关文章
- 数据结构与算法—Trie树
Trie,又经常叫前缀树,字典树等等.它有很多变种,如后缀树,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree.当然很多名字的意义其实有交 ...
- [算法] trie树实现
小写字母的字典树 #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXN 1 ...
- 洛谷 [USACO17OPEN]Bovine Genomics G奶牛基因组(金) ———— 1道骗人的二分+trie树(其实是差分算法)
题目 :Bovine Genomics G奶牛基因组 传送门: 洛谷P3667 题目描述 Farmer John owns NN cows with spots and NN cows without ...
- [算法]从Trie树(字典树)谈到后缀树
我是好文章的搬运工,原文来自博客园,博主July_,地址:http://www.cnblogs.com/v-July-v/archive/2011/10/22/2316412.html 从Trie树( ...
- 字符串模式匹配算法系列(三):Trie树及AC改进算法
Trie树的python实现(leetcode 208) #!/usr/bin/env python #-*- coding: utf-8 -*- import sys import pdb relo ...
- 算法笔记--字典树(trie 树)&& ac自动机 && 可持久化trie
字典树 简介:字典树,又称单词查找树,Trie树,是一种树形结构,是哈希树的变种. 优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较. 性质:根节点不包含字符,除根节点外每一个 ...
- 算法复习——trie树(poj2001)
题目: 题目描述 给出 n 个单词(1<=n<=1000),求出每个单词的非公共前缀,如果没有,则输出自己. 输入格式 输入 N 个单词,每行一个,每个单词都是由 1-20 个小写字母构成 ...
- 笔试算法题(39):Trie树(Trie Tree or Prefix Tree)
议题:TRIE树 (Trie Tree or Prefix Tree): 分析: 又称字典树或者前缀树,一种用于快速检索的多叉树结构:英文字母的Trie树为26叉树,数字的Trie树为10叉树:All ...
- 13-看图理解数据结构与算法系列(Trie树)
Trie树 Trie树,是一种搜索树,也称字典树或单词查找树,此外也称前缀树,因为某节点的后代存在共同的前缀.它的key都为字符串,能做到高效查询和插入,时间复杂度为O(k),k为字符串长度,缺点是如 ...
随机推荐
- Rom Modified [Galaxy 3 Tested]
1,Virtualbox虚拟机设置-数据空间注意这里不要勾选那个自动挂载,不然后面mount总会提示mount.vbox.. invalid argument. 2,进入ubuntu中,在终端下输入 ...
- 【转载】ASP.Net请求处理机制初步探索之旅 - Part 3 管道
开篇:上一篇我们了解了一个ASP.Net页面请求的核心处理入口,它经历了三个重要的入口,分别是:ISAPIRuntime.ProcessRequest().HttpRuntime.ProcessReq ...
- robot framework selenium2library定位
进行页面元素操作,最麻烦的莫过于元素定位了,经常提示element is not visible 或者element is not exist 下面介绍常见的定位方法和定位中的问题 1 使用name和 ...
- JS 的引用赋值与传值赋值
这个问题说大不大说小不小,如果你有幸踩了这个坑,一定会找这篇文章,哈哈~ 现说一下JS数字的类型:基本类型和引用类型 先看下下面两个栗子: var a = 30; var b = a; a = 20; ...
- String 的fomat方法日期转换
一.常规类型.字符类型和数值类型的格式说明符的语法如下:%[argument_index$][flags][width][.precision]conversion 可选的 argument_inde ...
- 【selenium+Python unittest】之使用smtplib发送邮件错误:smtplib.SMTPDataError:(554)、smtplib.SMTPAuthenticationError(例:126邮箱)
原代码如下: import smtplib from email.mime.text import MIMEText from email.header import Header #要发送的服务器 ...
- Android:实现两个Activity相互切换而都不走onCreate()
本文要实现的目的是: 有3个Activity: A,B,C.从A中能够进入B,B中能够进入C.而且B和C之间可能须要多次相互切换,因此不能使用普通的startActivity-finish方式,由于又 ...
- php eval()计算
php中的eval()函数可以处理php代码,因此可以用此来解决:以字符串格式存储的计算公式 比如: $str='2*(3+12)'; $s=eval("return $str;" ...
- live555二次开发经验总结:RTSPClient客户端与RTSPServer服务器
live555介绍 安防领域的流媒体开发者估计没有谁不知道live555的,可能并不是因为其架构有多牛,代码有多好看,而是因为这玩意存在的年限实在是太长了,从changelog来看,live555从2 ...
- C#中反射type记录
写代码的时候经常需要使用反射相关的东西例如:分析现有类型自动生成类, 或者为现有的类自动增加一些功能总结了一点点经验以ClassA a; 为例1. 通过typeof(ClassA) 或者 a.Get ...