Trie树(前缀树)的实现和应用

给定500万个单词,如何实现如下两个问题?

1、如何快速判断某个单词是否在给定的单词中?
2、如何快速的判断给定前缀有多少个单词?

一、树的构建

先思考一个问题,现在有两个单词,interest 和 interesting,会怎么存储?

其实如果数据量比较小时,用什么方式存储都可以,但是当数据量比较大或者涉及到快速查找,统计分析,网络传输等需求时,数据的存储方式,数据结构的实现等等就会变得很重要,这直接关乎到后续业务处理获取数据的效率。正如题目开始时说的,假如有500万个单词,该怎么存储的问题。那假如其中的两个单词是 interest 和 interesting,其实我们可以发现单词interest 和 interesting大部分是相同的,那对于相同的部分,我们可否共用同样的存储空间?其实是可以的,我们可以像下面这样进行表示

计算机领域,对于大数据量的存储,数据之间有高度的相似性时,可以把相同的部分抽取出来进行单独存储,这样可以节省一些空间,感觉设计模式中的 享元模式 就差不多是这个道理。

前面对单词interest 和 interesting的存储方案,可以把单词中重复的部分单独抽取出来进行共用(如interest),剩下的部分再进行单独的存储(如ing

那假如现在又来一个单词 interested,我们可以继续去树中进行查找,查找公共的最长前缀,做相应的处理。处理后如下图所示。我们可以看到 三个 单词 interest 、 interesting interested 复用了大量的存储空间。 

那假如现在又来了个单词 inside,我们可以按照相同的逻辑进行处理。找出最长前缀,进行相应的拆分处理。我们假设 in 不在500万单词中,那找到一个节点时如何确定是已经找到了单词几点,还是前缀,其实我们可以加标记进行区分,图中用不同的形状(圆形)表示非单词几点,矩形表示单词节点。具体实现时可以在节点中增加属性来进行区分。

 

那假如再来一个单词 apple ,现在apple没有与已存单词共用的前缀,则从根节点开始,另开一个分支,如下图所示。

二、查找(判断单词是否在其中)

假如树现在已经构造好了,假如目前就 5 个单词组成的树,如下所示

(1)查找单词 interested 是否在字典中

从跟开始,逐步遍历即可,遍历路劲如下图

(2)查找单词 internal ,根据如下路劲进行查找,查找 找到 in,还剩余 ternal ,发现没有以 t / te / ter /tern /terna / ternal 为前缀的,则查询结束。表示查不到。

 三、判断以某个字符串为前缀的单词有多少个

其实这个比较好搞,如果我们简单的只是需要知道有多少个,我们可以在单词进入字典树时就计算好,判断时直接获取就好,但是如果我们需要知道详细的明细,则需要进行整颗树的搜索。如下的需求,想获取以inter开头的并进行展示。

 结合上面分析,代码如下: 

package com.Ycb.tree;

import java.util.HashMap;

/**
 * 字典树
 */
public class DictionaryTree {
    /**
     * 字典树节点
     */
    private class Node {
        /**
         * 节点所代表的字符
         */
        private String str;

        //节点下单词数量
        private int count;

        //父节点
        private Node parent;

        //节点是否表示单词
        private boolean word;

        //孩子节点
        private HashMap<String, Node> childs;

        public Node() {
            this.childs = new HashMap<>();
        }

        public Node(String str, int count, boolean word) {
            this();
            this.count = count;
            this.word = word;
            this.str = str;
        }

        /**
         * 添加子节点,同时设置子节点的parent
         *
         * @param key
         * @param node
         */
        public void addChild(String key, Node node) {
            this.childs.put(key, node);
            node.parent = this;
        }

        /**
         * 移除子节点
         *
         * @param key
         */
        public void removeChild(String key) {
            //移除子节点
            this.childs.remove(key);
        }

        @Override
        public String toString() {
            return "Node{" + "str='" + str + '\'' + ", count=" + count + ", word=" + word + '}';
        }
    }

    //用一个哨兵节点表示根节点
    private Node root;

    public DictionaryTree() {
        //初始化哨兵跟节点
        root = new Node();
    }

    /**
     * 把word添加到字典树中
     *
     * @param word 待添加的单词
     */
    public void add(String word) {

        //最开始时从根节点开始进行查找插入
        addStr(word, root);
    }

    /**
     * 往node节点中插入word单词
     *
     * @param word 待插入的字串
     * @param node 待插入到的节点
     */
    private void addStr(String word, Node node) {
        node.count++;

        String str = node.str;
        if (str != null) {
            //寻找公共前缀
            String commonPrefix = "";
            for (int i = 0; i < word.length(); i++) {
                if (str.length() > i && word.charAt(i) == str.charAt(i)) {
                    commonPrefix += word.charAt(i);
                } else {
                    break;
                }
            }

            //找到公共前缀
            if (commonPrefix.length() > 0) {
                if (commonPrefix.length() == str.length() && commonPrefix.length() == word.length()) {
                    //与之前的元素重复
                    //需要递归向上使单词数减一
                    decreaseWordCount(node);
                } else if (commonPrefix.length() == str.length() && commonPrefix.length() < word.length()) {
                    //剩余的串
                    String wordLeft = word.substring(commonPrefix.length());

                    //继续去子节点中搜索
                    searchChild(wordLeft, node);
                } else if (commonPrefix.length() < str.length()) {
                    // 节点裂变
                    Node splitNode = new Node(commonPrefix, node.count, true);
                    // 处理裂变节点的父关系
                    splitNode.parent = node.parent;
                    splitNode.parent.addChild(commonPrefix, splitNode);
                    node.parent.removeChild(node.str);
                    node.count--;

                    // 节点裂变后的剩余字串
                    String strLeft = str.substring(commonPrefix.length());
                    node.str = strLeft;
                    splitNode.addChild(strLeft, node);
                    // 单词裂变后的剩余字串
                    if (commonPrefix.length() < word.length()) {
                        splitNode.word = false;
                        String wordLeft = word.substring(commonPrefix.length());
                        Node leftNode = new Node(wordLeft, 1, true);
                        splitNode.addChild(wordLeft, leftNode);
                    }
                }
            } else {
                //没有公共前缀,直接添加节点
                Node newNode = new Node(word, 1, true);
                node.addChild(word, newNode);
            }
        } else {
            //node代表跟节点
            if (node.childs.size() > 0) {
                //如果node已经有孩子节点,需要去孩子节点中继续查找
                searchChild(word, node);
            } else {
                //如果根节点还没有任何孩子,则直接添加
                Node newNode = new Node(word, 1, true);
                node.addChild(word, newNode);
            }
        }
    }

    /**
     * 在子节点中进行查找
     *
     * @param wordLeft
     * @param node
     */
    private void searchChild(String wordLeft, Node node) {
        boolean isFind = false;

        if (node.childs.size() > 0) {
            for (String childKey : node.childs.keySet()) {
                Node childNode = node.childs.get(childKey);
                if (wordLeft.charAt(0) == childNode.str.charAt(0)) {
                    isFind = true;
                    addStr(wordLeft, childNode);
                    break;
                }
            }
        }
        //如果没有搜索到
        if (isFind == false) {
            Node newNode = new Node(wordLeft, 1, true);
            node.addChild(wordLeft, newNode);
        }
    }

    /**
     * 查找单词是否在字典树中
     *
     * @param word 待查找的单词
     * @return
     */
    public boolean find(String word) {
        return findStr(word, root);
    }

    private boolean findStr(String word, Node node) {
        boolean isMatch = true;
        String wordLeft = word;
        String str = node.str;
        if (null != str) {
            // 字串与单词不匹配
            if (word.indexOf(str) != 0) {
                isMatch = false;
            } else {
                // 匹配,则计算剩余单词
                wordLeft = word.substring(str.length());
            }
        }
        // 如果匹配
        if (isMatch) {
            // 如果还有剩余单词长度
            if (wordLeft.length() > 0) {
                // 遍历孩子继续找
                for (String key : node.childs.keySet()) {
                    Node childNode = node.childs.get(key);
                    boolean isChildFind = findStr(wordLeft, childNode);
                    if (isChildFind) {
                        return true;
                    }
                }
                return false;
            } else {
                // 没有剩余单词长度,说明已经匹配完毕,直接返回节点是否为单词
                return node.word;
            }
        }
        return false;
    }

    // 统计子节点字串单词数
    private int countChildStr(String prefix, Node node) {
        // 遍历孩子
        for (String key : node.childs.keySet()) {
            Node childNode = node.childs.get(key);
            // 匹配子节点
            int childCount = countStr(prefix, childNode);
            if (childCount != 0) {
                return childCount;
            }
        }
        return 0;
    }

    // 统计字串单词数
    private int countStr(String prefix, Node node) {
        String str = node.str;
        // 非根结点
        if (null != str) {
            // 前缀与字串不匹配
            if (prefix.indexOf(str) != 0 && str.indexOf(prefix) != 0) {
                return 0;
                // 前缀匹配字串,且前缀较短
            } else if (str.indexOf(prefix) == 0) {
                // 找到目标节点,返回单词数
                return node.count;
                // 前缀匹配字串,且字串较短
            } else if (prefix.indexOf(str) == 0) {
                // 剩余字串继续匹配子节点
                String prefixLeft = prefix.substring(str.length());
                if (prefixLeft.length() > 0) {
                    return countChildStr(prefixLeft, node);
                }
            }
        } else {
            // 根结点,直接找其子孙
            return countChildStr(prefix, node);
        }
        return 0;
    }

    // 统计前缀单词数
    public int count(String prefix) {
        // 处理特殊情况
        if (null == prefix || prefix.trim().isEmpty()) {
            return root.count;
        }
        // 从根结点往下匹配
        return countStr(prefix, root);
    }

    // 打印节点
    private void printNode(Node node, int layer) {
        // 层级递进
        for (int i = 0; i < layer; i++) {
            System.out.print("    ");
        }
        // 打印
        System.out.println(node);
        // 递归打印子节点
        for (String str : node.childs.keySet()) {
            Node child = node.childs.get(str);
            printNode(child, layer + 1);
        }
    }

    // 打印字典树
    public void print() {
        printNode(root, 0);
    }

    /**
     * 使单词数减1
     *
     * @param node
     */
    private void decreaseWordCount(Node node) {
        while (node != null) {
            node.count--;
            node = node.parent;
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值