昨天看到一篇介绍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:\MyCode\sourcecode\dotnet\dotnet core\ConsoleAppCoreTest\ConsoleAppCoreTest\Files\words.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毫秒。速度确实还可以。如果数据多了的话,就有点费内存,很显然的是以空间换时间的算法

 

 posted on 2020-10-20 11:10  F风  阅读(156)  评论(0编辑  收藏  举报