• [算法]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("
    
    go 出现的次数为:{0}
    
    ", 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[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("
    
    go 出现的次数为:{0}
    
    ", 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[26];
                    freq = 0;
                }
            }
            #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 == 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 检索操作
            /// <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 == 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 统计指定单词出现的次数
    
            /// <summary>
            /// 统计指定单词出现的次数
            /// </summary>
            /// <param name="root"></param>
            /// <param name="word"></param>
            /// <returns></returns>
            public int WordCount(string word)
            {
                int count = 0;
    
                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 == 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 修改操作
            /// <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 == 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
        }
    }
    View Code
  • 相关阅读:
    动手动脑
    加减乘除
    测试
    Java学习的第五十六天
    Java学习的第五十五天
    js判断一个时间是否在某一个时间段内
    js计算两个时间相差多少分钟
    js分钟转化为小时并且以某个数字进行递增
    关于Js debounce(防抖)函数和throttle(节流)小结
    webpack配置scss
  • 原文地址:https://www.cnblogs.com/panweishadow/p/3722100.html
Copyright © 2020-2023  润新知