zoukankan      html  css  js  c++  java
  • Trie树详解

    1、 概述

      Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树。Trie一词来自retrieve,发音为/tri:/ “tree”,也有人读为/traɪ/ “try”。Trie树可以利用字符串的公共前缀来节约存储空间。如下图所示,该trie树用10个节点保存了6个字符串tea,ten,to,in,inn,int:

      在该trie树中,字符串in,inn和int的公共前缀是“in”,因此可以只存储一份“in”以节省空间。当然,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存,这也是trie树的一个缺点。Trie树的基本性质可以归纳为:

    1. 根节点不包含字符,除根节点以外每个节点只包含一个字符。
    2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
    3. 每个节点的所有子节点包含的字符串不相同。

    2、 Trie树的基本实现

      字母树的插入(Insert)、删除( Delete)和查找(Find)都非常简单,用一个一重循环即可,即第i次循环找到前i个字母所对应的子树,然后进行相应的操作。实现这棵字母树,我们用最常见的数组保存(静态开辟内存)即可,当然也可以开动态的指针类型(动态开辟内存)。 至于结点对儿子的指向,一般有三种方法:

    1. 对每个结点开一个字母集大小的数组,对应的下标是儿子所表示的字母,内容则是这个儿子对应在大数组上的位置,即标号;
    2. 对每个结点挂一个链表,按一定顺序记录每个儿子是谁;
    3. 使用左儿子右兄弟表示法记录这棵树。

      三种方法,各有特点。第一种易实现,但实际的空间要求较大;第二种,较易实现,空间要求相对较小,但比较费时;第三种,空间要求最小,但相对费时且不易写。

    3、 Trie树的高级实现

      可以采用双数组(Double-Array)实现。利用双数组可以大大减小内存使用量,具体实现细节见参考资料(5)(6)。

    4、 Trie树的应用

      Trie是一种非常简单高效的数据结构,但有大量的应用实例。

    (1) 字符串检索

      事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

      举例:

    • 给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
    •  给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

    (2)字符串最长公共前缀

      Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。

    举例:

    1. 给出N个小写英文字母串,以及Q个询问,即询问某两个串的最长公共前缀的长度是多少?

      解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

      而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

    1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
    2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;(关于并查集,Tarjan算法,RMQ问题,网上有很多资料。)

    (3)排序

      Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。给你N个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

    (4) 作为其他数据结构和算法的辅助结构

      如后缀树,AC自动机等

    5、 Trie树复杂度分析

    1. 插入、查找的时间复杂度均为O(N),其中N为字符串长度。
    2. 空间复杂度是26^n级别的,非常庞大(可采用双数组实现改善)。

    6、 总结

      Trie树是一种非常重要的数据结构,它在信息检索,字符串匹配等领域有广泛的应用,同时,它也是很多算法和复杂数据结构的基础,如后缀树,AC自动机等,因此,掌握Trie树这种数据结构,对于一名IT人员,显得非常基础且必要!

    7.简单实现

    package IO;
    
    public class Trie {
        private int SIZE = 26;
        private TrieNode root;// 字典树的根
        Trie(){ // 初始化字典树
            root = new TrieNode();
        }
    
        private class TrieNode{ //字典树节点
            private int num;// 有多少单词通过这个节点,即由根至该节点组成的字符串模式出现的次数
            private TrieNode[] son;// 所有的儿子节点
            private boolean isEnd;// 是不是最后一个节点
            private char val;// 节点的值
    
            TrieNode() {
                num = 1;
                son = new TrieNode[SIZE];
                isEnd = false;
            }
        }
    
        // 建立字典树
        public void insert(String str){// 在字典树中插入一个单词
            if (str == null || str.length() == 0) {
                return;
            }
            TrieNode node = root;
            char[] letters = str.toCharArray();
            for (int i = 0, len = str.length(); i < len; i++) {
                int pos = letters[i] - 'a';
                if (node.son[pos] == null) {
                    node.son[pos] = new TrieNode();
                    node.son[pos].val = letters[i];
                } else {
                    node.son[pos].num++;
                }
                node = node.son[pos];
            }
            node.isEnd = true;
        }
    
        // 计算单词前缀的数量
        public int countPrefix(String prefix) {
            if (prefix == null || prefix.length() == 0) {
                return -1;
            }
            TrieNode node = root;
            char[] letters = prefix.toCharArray();
            for (int i = 0, len = prefix.length(); i < len; i++) {
                int pos = letters[i] - 'a';
                if (node.son[pos] == null) {
                    return 0;
                } else {
                    node = node.son[pos];
                }
            }
            return node.num;
        }
    
        // 打印指定前缀的单词
        public String hasPrefix(String prefix) {
            if (prefix == null || prefix.length() == 0) {
                return null;
            }
            TrieNode node = root;
            char[] letters = prefix.toCharArray();
            for (int i = 0, len = prefix.length(); i < len; i++) {
                int pos = letters[i] - 'a';
                if (node.son[pos] == null) {
                    return null;
                } else {
                    node = node.son[pos];
                }
            }
            preTraverse(node, prefix);
            return null;
        }
    
        // 遍历经过此节点的单词.
        public void preTraverse(TrieNode node, String prefix) {
            if (!node.isEnd) {
                for (TrieNode child : node.son) {
                    if (child != null) {
                        preTraverse(child, prefix + child.val);
                    }
                }
                return;
            }
            System.out.println(prefix);
        }
    
        // 在字典树中查找一个完全匹配的单词.
        public boolean has(String str) {
            if (str == null || str.length() == 0) {
                return false;
            }
            TrieNode node = root;
            char[] letters = str.toCharArray();
            for (int i = 0, len = str.length(); i < len; i++) {
                int pos = letters[i] - 'a';
                if (node.son[pos] != null) {
                    node = node.son[pos];
                } else {
                    return false;
                }
            }
            return node.isEnd;
        }
    
        //前序遍历字典树.
        public void preTraverse(TrieNode node) {
            if (node != null) {
                System.out.print(node.val + "-");
                for (TrieNode child : node.son) {
                    preTraverse(child);
                }
            }
        }
    
        public TrieNode getRoot() {
            return this.root;
        }
    
        public static void main(String[] args) {
            Trie tree = new Trie();
            String[] strs = { "banana", "band", "bee", "absolute", "acm", };
            String[] prefix = { "ba", "b", "band", "abc", };
            for (String str : strs) {
                tree.insert(str);
            }
            System.out.println(tree.has("abc"));
            tree.preTraverse(tree.getRoot());
            System.out.println();
            for (String pre : prefix) {
                int num = tree.countPrefix(pre);
                System.out.println(pre + "" + num);
            }
        }
    }
    View Code
  • 相关阅读:
    直接插入排序
    简单选择排序
    xml错误: A pseudo attribute name is expecte
    接口, 抽象类
    The hierarchy of the type AudienceAdvice is inconsistent 《spring in action》
    spring配置文件出错解决办法
    Sangfor tester 实习总结
    求一棵树的面积,2014百度开发测试笔试题
    百度2014校招-深圳-开发测试-求最小“不重复数”
    二个数相加,不使用加减乘除
  • 原文地址:https://www.cnblogs.com/wxgblogs/p/5698423.html
Copyright © 2011-2022 走看看