Trie树

àì夳堔傛蜴生んèń 2024-02-18 23:58 170阅读 0赞

Trie树的名字有很多,比如字典树,前缀树等等。

一:概念

下面我们有and,as,at,cn,com这些关键词,那么如何构建trie树呢?

2012112521092438.png

从上面的图中,我们或多或少的可以发现一些好玩的特性。

第一:根节点不包含字符,除根节点外的每一个子节点都包含一个字符。

第二:从根节点到某一节点,路径上经过的字符连接起来,就是该节点对应的字符串。

第三:每个单词的公共前缀作为一个字符节点保存。

二:使用范围

既然学Trie树,我们肯定要知道这玩意是用来干嘛的。

第一:词频统计。

可能有人要说了,词频统计简单啊,一个hash或者一个堆就可以打完收工,但问题来了,如果内存有限呢?还能这么

玩吗?所以这里我们就可以用trie树来压缩下空间,因为公共前缀都是用一个节点保存的。

第二: 前缀匹配

就拿上面的图来说吧,如果我想获取所有以”a”开头的字符串,从图中可以很明显的看到是:and,as,at,如果不用trie树,

你该怎么做呢?很显然朴素的做法时间复杂度为O(N2) ,那么用Trie树就不一样了,它可以做到h,h为你检索单词的长度,

可以说这是秒杀的效果。

举个例子:现有一个编号为1的字符串”and“,我们要插入到trie树中,采用动态规划的思想,将编号”1“计入到每个途径的节点中,

那么以后我们要找”a“,”an“,”and”为前缀的字符串的编号将会轻而易举。

2012112521371883.png

三:实际操作

到现在为止,我想大家已经对trie树有了大概的掌握,下面我们看看如何来实现。

1:定义trie树节点

为了方便,我也采用纯英文字母,我们知道字母有26个,那么我们构建的trie树就是一个26叉树,每个节点包含26个子节点。

复制代码

  1. 1 #region Trie树节点
  2. 2 /// <summary>
  3. 3 /// Trie树节点
  4. 4 /// </summary>
  5. 5 public class TrieNode
  6. 6 {
  7. 7 /// <summary>
  8. 8 /// 26个字符,也就是26叉树
  9. 9 /// </summary>
  10. 10 public TrieNode[] childNodes;
  11. 11
  12. 12 /// <summary>
  13. 13 /// 词频统计
  14. 14 /// </summary>
  15. 15 public int freq;
  16. 16
  17. 17 /// <summary>
  18. 18 /// 记录该节点的字符
  19. 19 /// </summary>
  20. 20 public char nodeChar;
  21. 21
  22. 22 /// <summary>
  23. 23 /// 插入记录时的编码id
  24. 24 /// </summary>
  25. 25 public HashSet<int> hashSet = new HashSet<int>();
  26. 26
  27. 27 /// <summary>
  28. 28 /// 初始化
  29. 29 /// </summary>
  30. 30 public TrieNode()
  31. 31 {
  32. 32 childNodes = new TrieNode[26];
  33. 33 freq = 0;
  34. 34 }
  35. 35 }
  36. 36 #endregion

复制代码

2: 添加操作

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

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

复制代码

  1. 1 /// <summary>
  2. 2 /// 插入操作
  3. 3 /// </summary>
  4. 4 /// <param name="root"></param>
  5. 5 /// <param name="s"></param>
  6. 6 public void AddTrieNode(ref TrieNode root, string word, int id)
  7. 7 {
  8. 8 if (word.Length == 0)
  9. 9 return;
  10. 10
  11. 11 //求字符地址,方便将该字符放入到26叉树中的哪一叉中
  12. 12 int k = word[0] - 'a';
  13. 13
  14. 14 //如果该叉树为空,则初始化
  15. 15 if (root.childNodes[k] == null)
  16. 16 {
  17. 17 root.childNodes[k] = new TrieNode();
  18. 18
  19. 19 //记录下字符
  20. 20 root.childNodes[k].nodeChar = word[0];
  21. 21 }
  22. 22
  23. 23 //该id途径的节点
  24. 24 root.childNodes[k].hashSet.Add(id);
  25. 25
  26. 26 var nextWord = word.Substring(1);
  27. 27
  28. 28 //说明是最后一个字符,统计该词出现的次数
  29. 29 if (nextWord.Length == 0)
  30. 30 root.childNodes[k].freq++;
  31. 31
  32. 32 AddTrieNode(ref root.childNodes[k], nextWord, id);
  33. 33 }
  34. 34 #endregion

复制代码

3:删除操作

删除操作中,我们不仅要删除该节点的字符串编号,还要对词频减一操作。

复制代码

  1. /// <summary>
  2. /// 删除操作
  3. /// </summary>
  4. /// <param name="root"></param>
  5. /// <param name="newWord"></param>
  6. /// <param name="oldWord"></param>
  7. /// <param name="id"></param>
  8. public void DeleteTrieNode(ref TrieNode root, string word, int id)
  9. {
  10. if (word.Length == 0)
  11. return;
  12. //求字符地址,方便将该字符放入到26叉树种的哪一颗树中
  13. int k = word[0] - 'a';
  14. //如果该叉树为空,则说明没有找到要删除的点
  15. if (root.childNodes[k] == null)
  16. return;
  17. var nextWord = word.Substring(1);
  18. //如果是最后一个单词,则减去词频
  19. if (word.Length == 0 && root.childNodes[k].freq > 0)
  20. root.childNodes[k].freq--;
  21. //删除途经节点
  22. root.childNodes[k].hashSet.Remove(id);
  23. DeleteTrieNode(ref root.childNodes[k], nextWord, id);
  24. }

复制代码

4:测试

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

复制代码

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

复制代码

2012112522045926.png

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

2012112522115572.png

测试文件: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[0]));
}

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树节点
///


/// Trie树节点
///

public class TrieNode
{
///
/// 26个字符,也就是26叉树
///

public TrieNode[] childNodes;

///


/// 词频统计
///

public int freq;

///


/// 记录该节点的字符
///

public char nodeChar;

///


/// 插入记录时的编号id
///

public HashSet hashSet = new HashSet();

///


/// 初始化
///

public TrieNode()
{
childNodes = new TrieNode[26];
freq = 0;
}
}
#endregion

#region 插入操作
///


/// 插入操作
///

///
///
public void AddTrieNode(string word, int id)
{
AddTrieNode(ref trieNode, word, id);
}

///


/// 插入操作
///

///
///
public void AddTrieNode(ref TrieNode root, string word, int id)
{
if (word.Length == 0)
return;

//求字符地址,方便将该字符放入到26叉树中的哪一叉中
int k = word[0] - ‘a’;

//如果该叉树为空,则初始化
if (root.childNodes[k] == null)
{
root.childNodes[k] = new TrieNode();

//记录下字符
root.childNodes[k].nodeChar = word[0];
}

//该id途径的节点
root.childNodes[k].hashSet.Add(id);

var nextWord = word.Substring(1);

//说明是最后一个字符,统计该词出现的次数
if (nextWord.Length == 0)
root.childNodes[k].freq++;

AddTrieNode(ref root.childNodes[k], nextWord, id);
}
#endregion

#region 检索操作
///


/// 检索单词的前缀,返回改前缀的Hash集合
///

///
///
public HashSet SearchTrie(string s)
{
HashSet hashSet = new HashSet();

return SearchTrie(ref trieNode, s, ref hashSet);
}

///


/// 检索单词的前缀,返回改前缀的Hash集合
///

///
///
///
public HashSet SearchTrie(ref TrieNode root, string word, ref HashSet hashSet)
{
if (word.Length == 0)
return hashSet;

int k = word[0] - ‘a’;

var nextWord = word.Substring(1);

if (nextWord.Length == 0)
{
//采用动态规划的思想,word最后节点记录这途经的id
hashSet = root.childNodes[k].hashSet;
}

SearchTrie(ref root.childNodes[k], nextWord, ref hashSet);

return hashSet;
}
#endregion

#region 统计指定单词出现的次数

///


/// 统计指定单词出现的次数
///

///
///
///
public int WordCount(string word)
{
int count = 0;

WordCount(ref trieNode, word, ref count);

return count;
}

///


/// 统计指定单词出现的次数
///

///
///
///
///
public void WordCount(ref TrieNode root, string word, ref int count)
{
if (word.Length == 0)
return;

int k = word[0] - ‘a’;

var nextWord = word.Substring(1);

if (nextWord.Length == 0)
{
//采用动态规划的思想,word最后节点记录这途经的id
count = root.childNodes[k].freq;
}

WordCount(ref root.childNodes[k], nextWord, ref count);
}

#endregion

#region 修改操作
///


/// 修改操作
///

///
///
///
public void UpdateTrieNode(string newWord, string oldWord, int id)
{
UpdateTrieNode(ref trieNode, newWord, oldWord, id);
}

///


/// 修改操作
///

///
///
///
///
public void UpdateTrieNode(ref TrieNode root, string newWord, string oldWord, int id)
{
//先删除
DeleteTrieNode(oldWord, id);

//再添加
AddTrieNode(newWord, id);
}
#endregion

#region 删除操作
///


/// 删除操作
///

///
///
///
///
public void DeleteTrieNode(string word, int id)
{
DeleteTrieNode(ref trieNode, word, id);
}

///


/// 删除操作
///

///
///
///
///
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);
}
#endregion
}
}

发表评论

表情:
评论列表 (有 0 条评论,170人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Trie

    Trie树的名字有很多,比如字典树,前缀树等等。 一:概念 下面我们有and,as,at,cn,com这些关键词,那么如何构建trie树呢? ![201211252109

    相关 Trie

    Trie树 201227 > 思路 题目 答案 题目: 输入的第一行为一个正整数n,表示词典的大小,其后n行,每一行一个单词(不保证是英文单词,也有可能是火星文单

    相关 Trie

    定义:又称字典树或单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频

    相关 Trie(字典

    1. Trie树 Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索

    相关 trie

    输入 输入的第一行为一个正整数n,表示词典的大小,其后n行,每一行一个单词(不保证是英文单词,也有可能是火星文单词哦),单词由不超过10个的小写英文字母组成,可能存在相同的单