字典树(java实现)

一、概念:

Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

    Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。


二、3个基本性质:


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

三、Trie树的构建


     本质上,Trie是一颗存储多个字符串的树。相邻节点间的边代表一个字符,这样树的每条分支代表一则子串,而树的叶节点则代表完整的字符串。和普通树不同的地方是,相同的字符串前缀共享同一条分支。举一个例子。给出一组单词,inn, int, at, age, adv, ant, 我们可以得到下面的Trie:

    搭建Trie的基本算法很简单,无非是逐一把每则单词的每个字母插入Trie。插入前先看前缀是否存在。如果存在,就共享,否则创建对应的节点和边。比如要插入单词add,就有下面几步:
    1.考察前缀"a",发现边a已经存在。于是顺着边a走到节点a。
    2.考察剩下的字符串"dd"的前缀"d",发现从节点a出发,已经有边d存在。于是顺着边d走到节点ad
    3.考察最后一个字符"d",这下从节点ad出发没有边d了,于是创建节点ad的子节点add,并把边ad->add标记为d。
 

插入:

插入操作就是将单词的每个字母都逐一插入Trie树,插入前看这个字母对应的节点是否存在,若不存在就新建一个节点,否则就共享那一个节点,还是以下图为例:

假如说我们要在原Trie树中新插入一个单词and,那我们的操作为:

  1. 插入第一个字母a,发现根节点存在子节点a,则共享节点a
  2. 插入第二个字母n,发现节点a不存在子节点n,则新建子节点n
  3. 插入第三个字母d,发现节点n不存在子节点d,则新建子节点d

查询:

查询操作和插入操作其实差不多,就是在Trie树中找这个单词的每个字母,若找到了就继续找下去,若没有找到就可以直接退出了,因为若没找到就说明没有这个单词,还还还是以下图为例:

 

【复杂度分析】

Trie树其实是一种用空间换时间的算法,前面也提到过,它占用的空间一般很大,但时间是非常高效的,插入和查询的时间复杂度都是O(l)的,总体来说还是很优秀的

 

import java.util.HashMap;
public class Trie_Tree{
    /**
     * 内部节点类
     * @author "zhshl"
     * @date	2014-10-14
     *
     */
    private class Node{
        private int dumpli_num;
        该字串的重复数目,  该属性统计重复次数的时候有用,取值为0、1、2、3、4、5……
        private int prefix_num;
        ///以该字串为前缀的字串数, 应该包括该字串本身!!!!!
        private Node childs[];
        此处用数组实现,当然也可以map或list实现以节省空间
        private boolean isLeaf;
        ///是否为单词节点
        public Node(){
            dumpli_num=0;
            prefix_num=0;
            isLeaf=false;
            childs=new Node[26];
        }
    }

    private Node root;
    ///树根
    public Trie_Tree(){
        ///初始化trie 树
        root=new Node();
    }

    /**
     * 插入字串,用循环代替迭代实现
     * @param words
     */
    public void insert(String words){
        insert(this.root, words);
    }
    /**
     * 插入字串,用循环代替迭代实现
     * @param root
     * @param words
     */
    private void insert(Node root,String words){
        words=words.toLowerCase();
        转化为小写
        char[] chrs=words.toCharArray();

        for(int i=0,length=chrs.length; i<length; i++){
            ///用相对于a字母的值作为下标索引,也隐式地记录了该字母的值
            int index=chrs[i]-'a';
            if(root.childs[index]!=null){
                已经存在了,该子节点prefix_num++
                root.childs[index].prefix_num++;
            }else{
                ///如果不存在
                root.childs[index]=new Node();
                root.childs[index].prefix_num++;
            }

            ///如果到了字串结尾,则做标记
            if(i==length-1){
                root.childs[index].isLeaf=true;
                root.childs[index].dumpli_num++;
            }
            ///root指向子节点,继续处理
            root=root.childs[index];
        }

    }

    /**
     * 遍历Trie树,查找所有的words以及出现次数
     * @return HashMap<String, Integer> map
     */
    public HashMap<String,Integer> getAllWords(){
        return preTraversal(this.root, "");
    }

    /**
     * 前序遍历。。。
     * @param root		子树根节点
     * @param prefixs	查询到该节点前所遍历过的前缀
     * @return
     */
    private  HashMap<String,Integer> preTraversal(Node root,String prefixs){
        HashMap<String, Integer> map=new HashMap<>();

        if(root!=null){

            if(root.isLeaf==true){
                当前即为一个单词
                map.put(prefixs, root.dumpli_num);
            }

            for(int i=0,length=root.childs.length; i<length;i++){
                if(root.childs[i]!=null){
                    char ch=(char) (i+'a');
                    递归调用前序遍历
                    String tempStr=prefixs+ch;
                    map.putAll(preTraversal(root.childs[i], tempStr));
                }
            }
        }

        return map;
    }

    /**
     * 判断某字串是否在字典树中
     * @param word
     * @return true if exists ,otherwise  false
     */
    public boolean isExist(String word){
        return search(this.root, word);
    }
    /**
     * 查询某字串是否在字典树中
     * @param word
     * @return true if exists ,otherwise  false
     */
    private boolean search(Node root,String word){
        char[] chs=word.toLowerCase().toCharArray();
        for(int i=0,length=chs.length; i<length;i++){
            int index=chs[i]-'a';
            if(root.childs[index]==null){
                ///如果不存在,则查找失败
                return false;
            }
            root=root.childs[index];
        }

        return true;
    }

    /**
     * 得到以某字串为前缀的字串集,包括字串本身! 类似单词输入法的联想功能
     * @param prefix 字串前缀
     * @return 字串集以及出现次数,如果不存在则返回null
     */
    public HashMap<String, Integer> getWordsForPrefix(String prefix){
        return getWordsForPrefix(this.root, prefix);
    }
    /**
     * 得到以某字串为前缀的字串集,包括字串本身!
     * @param root
     * @param prefix
     * @return 字串集以及出现次数
     */
    private HashMap<String, Integer> getWordsForPrefix(Node root,String prefix){
        HashMap<String, Integer> map=new HashMap<>();
        char[] chrs=prefix.toLowerCase().toCharArray();
        
        for(int i=0, length=chrs.length; i<length; i++){

            int index=chrs[i]-'a';
            if(root.childs[index]==null){
                return null;
            }

            root=root.childs[index];

        }
        ///结果包括该前缀本身
        ///此处利用之前的前序搜索方法进行搜索
        return preTraversal(root, prefix);
    }

    public static void main(String args[]){
        Trie_Tree trie = new Trie_Tree();
        trie.insert("I");
        trie.insert("Love");
        trie.insert("China");
        trie.insert("China");
        trie.insert("China");
        trie.insert("China");
        trie.insert("China");
        trie.insert("xiaoliang");
        trie.insert("xiaoliang");
        trie.insert("man");
        trie.insert("handsome");
        trie.insert("love");
        trie.insert("chinaha");
        trie.insert("her");
        trie.insert("know");

        HashMap<String,Integer> map=trie.getAllWords();

        for(String key:map.keySet()){
            System.out.println(key+" 出现: "+ map.get(key)+"次");
        }


        map=trie.getWordsForPrefix("chin");

        System.out.println("\n\n包含chin(包括本身)前缀的单词及出现次数:");
        for(String key:map.keySet()){
            System.out.println(key+" 出现: "+ map.get(key)+"次");
        }

        if(trie.isExist("xiaoming")==false){
            System.out.println("\n\n字典树中不存在:xiaoming ");
        }


    }
}

love 出现: 2次
handsome 出现: 1次
china 出现: 5次
her 出现: 1次
xiaoliang 出现: 2次
i 出现: 1次
know 出现: 1次
man 出现: 1次
chinaha 出现: 1次


包含chin(包括本身)前缀的单词及出现次数:
china 出现: 5次
chinaha 出现: 1次


字典树中不存在:xiaoming 

Process finished with exit code 0

 

参考文献

https://blog.csdn.net/abcd_d_/article/details/40116485?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-12.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-12.nonecase