zoukankan      html  css  js  c++  java
  • TrieTree树

    昨天看到一篇介绍TrieTree树的文章,感觉还不错的。于是小小研究了一下,并用C#实现了相关代码。

    TrieTree树提供对字符的存储和遍历,可以用来查询词汇出现的频率或实现其他功能,速度还挺快。最特色的是,如果有相同前缀词的字符串,则它们的前缀部分可以被共享存储。

    TrieTree类是一个单例类,对外提供对字符串的增/删,词语频次的查询,获取根节点下的所有词汇等等。客户端调用TreeTree单例类即可。

    TrieTree类代码如下:

        //TrieTree:单例类
        //1.提供对字符串的增/删;
        //2.词语频次的查询;
        //3.节点下所有词汇的获取;
        public sealed class TrieTree
        {
            private static TrieTree _instance;
            private static object _locker=new object();
            private readonly ITrieTreeNode _root;
    
            private TrieTree()
            {
                _root=new TrieTreeNode();
            }
    
            public static TrieTree Instence
            {
                get
                {
                    if (_instance == null)
                    {
                        lock (_locker)
                        {
                            if(_instance==null)
                                _instance=new TrieTree();
                        }
                    }
    
                    return _instance;
                }
            }
    
            //添加 字符串
            public void AddWord(string word)
            {
                _root.AddNode(word);
            }
    
            //移除 字符串
            public void RemoveWord(string word)
            {
                _root.Remove(word);
            }
    
            //获取该词语被添加的频次
            public int GetFrequency(string word)
            {
                return _root.GetFrequency(word);
            }
    
            //获取前缀词语被添加的频次
            public int GetFrequencyByPrefix(string word)
            {
                return _root.GetFrequencyByPrefix(word);
            }
    
            //获取根节点下的所有词汇
            public List<string> GetWords()
            {
                return _root.GetWords();
            }
        }

    ITrieTreeNode提供TrieTreeNode节点的对外方法,代码如下所示:

        public interface ITrieTreeNode
        {
            //添加词语
            void AddNode(string word);
    
            //移除词语
            void Remove(string word);
    
            //获取该词语被添加的频次
            int GetFrequency(string word);
    
            //获取前缀词语被添加的频次
            int GetFrequencyByPrefix(string prefixWord);
    
            //从该节点开始,获取该节点下的所有词汇
            List<string> GetWords();
    
            //从最后的一个节点往上搜索,组装每一个节点的字符,并返回一个字符串
            string GetWord(TrieTreeNode lastNode);
        }

    真正的操作还在TrieTreeNode类中,这是一个节点,提供存储字符和其他数据的功能。代码如下所示:

       public class TrieTreeNode: ITrieTreeNode {
            //字符
            public char NodeChar { get; private set; }
    
            //作为前缀的使用频次
            private int _frequencyByPrefix = 0;
            public int FrequencyByPrefix
            {
                get => _frequencyByPrefix;
                private set => _frequencyByPrefix = value;
            }
    
            //作为词语的使用频次
            private int _frequency = 0;
            public int Frequency
            {
                get => _frequency;
                private set => _frequency = value;
            }
            //子节点
            public HashSet<TrieTreeNode> Children { get; private set; }
            //父节点
            public TrieTreeNode Parent { get; private set; }
    
            //判定 当前节点 是否为 词语的 最后一个字符节点。
            //对删除节点有用,在删除节点的时候,需要判断是否存在该语句,即找到的最后一个字符是否为终结点字符,如果不是,则不能删除,因为不存在该词语
            private bool _isTerminate = false;
    
            //实例化一个节点
            public TrieTreeNode()
            {
                Frequency = 0;
                Children = new HashSet<TrieTreeNode>();
            }
    
            //新增一个 字符
            private TrieTreeNode AddNode(char nodeChar)
            {
                //找到子节点
                var node = FindNode(nodeChar);
                if (node == null)
                {
                    node=new TrieTreeNode {NodeChar = nodeChar,Parent = this};
                    Children.Add(node);
                }
                //节点的前缀频次自增1
                node.AddFrequencyByPrefix();
    
                return node;
            }
    
            //新增 字符串
            public void AddNode(string word)
            {
                if (string.IsNullOrEmpty(word)) return;
                //获取首字符
                var firstChar = word[0];
                //将首字符新增到节点中
                var node = AddNode(firstChar);
                //循环添加 节点链
                for (int i = 1; i < word.Length; i++)
                {
                    var child = node.AddNode(word[i]);
                    node = child;
                }
    
                //最后一个节点设置为 词语的终结点
                node._isTerminate = true;
                //节点的全词频次自增1
                node.AddFrequency();
            }
    
            //移除 字符串
            public void Remove(string word)
            {
                RemoveWord(this, word);
            }
    
            private bool RemoveWord(TrieTreeNode parent,string word)
            {
                //如果字符串为空,则返回
                if (string.IsNullOrEmpty(word)) return false;
    
                //寻找子节点
                var node = parent.FindNode(word[0]);
                if (node == null) return false;
    
                //往下搜索
                var canRemove = RemoveWord(node, word.Substring(1));
    
                //如果找到最后的一个子节点不是终结点,则不用删除
                if ((node._isTerminate && word.Length==1) || canRemove)
                {
                    if(node._isTerminate && word.Length == 1)
                        node.DecFrequency();
                    //往下搜索完毕之后,回到该子节点
                    //该节点的前缀频次减1
                    node.DecFrequencyByPrefix();
    
                    //如果 前缀频次为0,则删除该节点
                    if (node._frequencyByPrefix <= 0)
                        parent.Children.Remove(node);
    
                    return true;
                }
    
                return false;
            }
    
            //找出词语添加的频次
            public int GetFrequency(string word) {
                return GetFrequency(word, false);
            }
    
            //获取前缀词语被添加的频次
            public int GetFrequencyByPrefix(string prefixWord) {
                return GetFrequency(prefixWord, true);
            }
    
            //找出词语添加的频次 isPrefix:是否为前缀词语
            private int GetFrequency(string word, bool isPrefix) {
                //如果字符串为空,则返回0
                if (string.IsNullOrEmpty(word)) return 0;
    
                //从首字符开始查找
                var node = FindNode(word[0]);
                //如果不存在首字符,则返回0
                if (node == null) return 0;
    
                //从第二个字符开始查找
                for (var index = 1; index < word.Length; index++) {
                    var child = node.FindNode(word[index]);
                    //如果在查找字符的时候中断,则证明无该词语,返回0
                    if (child == null) return 0;
                    //遍历下一个子节点
                    node = child;
                }
    
                //如果找到最后的一个子节点 是终结点,则返回最后一个节点的全词频次
                if (node._isTerminate)
                    return node?.Frequency ?? 0;
    
                ////如果找到最后的一个子节点 不是终结点,则返回最后一个节点的前缀词频次
                if (isPrefix)
                    return node?.FrequencyByPrefix ?? 0;
    
                //没找到,则返回0
                return 0;
            }
    
            //从该节点开始,获取该节点下的所有词汇
            public List<string> GetWords()
            {
                return GetWords(this);
            }
    
            private List<string> GetWords(TrieTreeNode node)
            {
                List<string> words=new List<string>();
                if (node == null) return words;
    
                //如果当前节点为词语的终结点,则获取该词语
                if(node._isTerminate)
                    words.Add(GetWord(node));
                //遍历每一个子节点
                foreach (var trieTreeNode in node.Children)
                {
                    words.AddRange(GetWords(trieTreeNode));
                }
    
                return words;
            }
    
            //从最后的一个节点往上搜索,组装每一个节点的字符,并返回一个字符串
            public string GetWord(TrieTreeNode lastNode)
            {
                //如果当前节点不存在,或者为最顶层的根节点,则返回空字符串
                if (lastNode == null || lastNode._frequencyByPrefix<=0) return string.Empty;
                return $"{GetWord(lastNode.Parent)}{lastNode.NodeChar}";
            }
    
            //查找 字符 的节点
            private TrieTreeNode FindNode(char nodeChar) => Children.FirstOrDefault(it => it.NodeChar == nodeChar);
    
            //频次自增1
            private void AddFrequency() {
                this.Frequency=Interlocked.Increment(ref _frequency);
            }
    
            //前缀频次自增1
            private void AddFrequencyByPrefix() {
                this.FrequencyByPrefix = Interlocked.Increment(ref _frequencyByPrefix);
            }
    
            //频次自减1
            private void DecFrequency()
            {
                this.Frequency = Interlocked.Decrement(ref _frequency);
            }
    
            //前缀频次自减1
            private void DecFrequencyByPrefix() {
                this.FrequencyByPrefix = Interlocked.Decrement(ref _frequencyByPrefix);
            }
    
            public override string ToString()
            {
                return NodeChar.ToString();
            }
        }

    以下是测试代码: 

                Stopwatch sw = new Stopwatch();
                sw.Start();
    
                var contents = File.ReadAllText(@"D:MyCodesourcecodedotnetdotnet coreConsoleAppCoreTestConsoleAppCoreTestFileswords.txt");
                var splitChars = new [] {' ',',','.',':',';','(',')','@','#','$','%','\','/','"','<','>' };
                var words = contents.Split(splitChars, StringSplitOptions.RemoveEmptyEntries);
                Console.WriteLine($"从文件中共获取{words.Length}条词汇");
    
                if (words?.Length > 0) {
                    foreach (var word in words) {
                        TrieTree.Instence.AddWord(word);
                    }
                }
                sw.Stop();
                Console.WriteLine($"新增数据花费了{sw.Elapsed.TotalSeconds}秒");
    
                sw.Reset();
                sw.Start();
                var list = TrieTree.Instence.GetWords();
                sw.Stop();
                
                Console.WriteLine($"从TrieTree中共获取{list.Count}条词汇");
                Console.WriteLine($"获取数据花费了{sw.Elapsed.TotalSeconds}秒");
    
                list.ForEach(it => { Console.Write($"{it} "); });
    
                Console.ReadKey();

    测试的结果如下图所示:

     为啥从文件中获取了3447条词汇,但从TrieTree中只获取到了1111条词汇?因为我实现的TrieTree是去重了的,即相同的字符串只获取一次。

    从测试结果中可以看出,数据在新增的时候花费了24.5984毫秒,读取是1111条数据花费了7毫秒。速度确实还可以。如果数据多了的话,就有点费内存,很显然的是以空间换时间的算法

  • 相关阅读:
    基于opencv的摄像头的标定
    图像的角点简介
    周转时间和平均带权时间等
    QT各个版本的下载的地址
    参考文献格式
    sublime中的emmet插件的使用技巧
    sublime快捷键
    CSS布局居中
    Markdown 语法说明(简体中文版)
    sql作业题
  • 原文地址:https://www.cnblogs.com/williamwsj/p/13845374.html
Copyright © 2011-2022 走看看