数据结构之前缀树

写在前面

源码

前缀树,又叫做trie树,字典树,是一种多叉的树,一般用于单词前缀匹配的相关场景中,比如:

本文看下使用Java如何来实现这种数据结构。

1:基本介绍

思想:空间换时间,因为需要维护非常多的引用,所以比较占用空间,但能够快速定位所以时间较短
时间复杂度:log
特点:
    根节点不包含字符
    每一条路径所有节点的字符拼接在一起就对应一个字符串
    拥有相同前缀的多个字符串共享相同前缀

结构如下:

2:代码实现

定义节点类:

java 复制代码
public class TreeNode {
    //经过这个节点的字符串的个数(以这个节点为前缀的字符串的个数)
    public int path;
    //以这个节点结束的字符串的个数(有多少个字符串有这条路径的char组成)
    public int end;
    //对应着小写的a-z的26个字母(如果要更多可以使用hashmap<char,Node>
    public TreeNode[] next;
    // 是否为叶子节点
    public boolean isLeaf = true;
    // 是否为一个单词的结束字符
    public boolean isWordEnd = false;
    public TreeNode() {
        path = 0;
        end = 0;
        next = new TreeNode[26];
    }

    @Override
    public String toString() {
        return "TreeNode{" +
                "path=" + path +
                ", end=" + end +
                ", next=" + Arrays.toString(next) +
                '}';
    }
}

定义前缀树类:

java 复制代码
public class TrieTree {

    public TreeNode root;

    public TrieTree() {
        root = new TreeNode();
    }


    /**
     * 在前缀树中插入字符串
     * 这种++的方法,导致,一个node,有多少个end,就有多少个相同的字符串
     * 一个node,有多少个path,就有多少个字符串经过(root的path代表有多少个字符串)(字符串末尾的node的path也会++)
     *
     * @param string 被插入的字符串(以前插入过的也可以插入)
     */
    public void insertString(String string) {
        if (string == null || string.length() == 0) {
            return;
        }
        int length = string.length();
        TreeNode nowNode = root;
        for (int i = 0; i < length; i++) {
            char now = string.charAt(i);
            int index = now - 'a';
            //index为字符now所处的位置
            if (nowNode.next[index] == null) {
                nowNode.next[index] = new TreeNode();
            }
            nowNode.isLeaf = false;
            // 先对当前node的path++,再转移到下一个node
            nowNode.path++;
            nowNode = nowNode.next[index];
        }
        // 处理 ab abc ,通过前缀a查询,也需要查询出ab的情况
        nowNode.isWordEnd = true;
        //在最后的node,path和end++
        nowNode.path++;
        nowNode.end++;
    }

    /**
     * 返回这个前缀树总共插入了多少个字符串
     *
     * @return
     */
    public int size() {
        return root.path;
    }

    /**
     * 前缀树查询总共插入这个字符串多少次,如果没插入过,则返回0
     *
     * @param string
     * @return
     */
    public int getStringNum(String string) {
        if (string == null || string.length() == 0) {
            return 0;
        }
        int length = string.length();
        TreeNode nowNode = root;
        for (int i = 0; i < length; i++) {
            char now = string.charAt(i);
            int index = now - 'a';
            //如果没有这个节点,说明不存在,直接返回0
            if (nowNode.next[index] == null) {
                return 0;
            }
            nowNode = nowNode.next[index];
        }
        //此时nowNode已经处于最后一个节点
        return nowNode.end;
    }

    /**
     * 前缀树查询以这个字符串为前缀的字符串总共多少个(包括以他为结尾的)
     *
     * @param string 前缀
     * @return
     */
    public int getPrefixNum(String string) {
        if (string == null || string.length() == 0) {
            return 0;
        }
        int length = string.length();
        TreeNode nowNode = root;
        for (int i = 0; i < length; i++) {
            char now = string.charAt(i);
            int index = now - 'a';
            //如果没有这个节点,说明前缀不存在,直接返回0
            if (nowNode.next[index] == null) {
                return 0;
            }
            nowNode = nowNode.next[index];
        }
        //此时nowNode已经处于前缀的最后一个节点
        return nowNode.path;
    }

//    public List<String> findByPrefix(String prefix) {
    public Set<String> findByPrefix(String prefix) {
        // 注意:根节点不存储任何元素
        TreeNode curNode = root;

        int prefixLen = prefix.length();
        // 1:找到prefix对应的TreeNode对象
        for (int i = 0; i < prefixLen; i++) {
            int idx = prefix.charAt(i) - 'a';
            TreeNode[] dataArr = curNode.next;
            if (dataArr[idx] == null) {
                System.out.println("not find!");
                return null;
            }
            // 非前缀的最后一个元素,遇到空,则说明要匹配的前缀不存在
            /*if (dataArr[idx] != null) {
                if (i > prefixLen - 1) {
                    return null;
                } else {
                    curNode = dataArr[idx];
                }
            }*/
            // 继续向下
            curNode = dataArr[idx];
        }
        // 2:根据prefix对应的TreeNode对象,递归找到所有的可能字符串
        TreeNode[] possibleTreeNodeArr = curNode.next;
        // 3:递归找到所有的可能字符串
//        List<String> possibleStrList = new ArrayList<>();
        Set<String> possibleStrList = new HashSet<>();
        /*for (int i = 0; i < possibleTreeNodeArr.length; i++) {
            if (possibleTreeNodeArr[i] != null) possibleStrList.add(prefix + (char) (i + 'a'));
        }
        for (int i = 0; i < possibleTreeNodeArr.length; i++) {
            queryAllPossibleStr(i, possibleTreeNodeArr, possibleStrList, prefix);
        }*/
        queryAllPossibleStr(0, curNode, possibleTreeNodeArr, possibleStrList, prefix);
        return possibleStrList;
    }

    private void queryAllPossibleStr(int i, TreeNode curNode, TreeNode[] possibleTreeNodeArr, Set<String> possibleStrList, String prefix) {
        if (i >= possibleTreeNodeArr.length || possibleTreeNodeArr == null) return;
        String newPrefix = prefix + (char) (i + 'a');
        // 元素为null,说明到达叶子节点
        if ((possibleTreeNodeArr[i] == null && curNode.isLeaf) || curNode.isWordEnd) {
//        if (possibleTreeNodeArr[i] == null && i == possibleTreeNodeArr.length - 1) {
//        if (possibleTreeNodeArr[i] != null && possibleTreeNodeArr[i].isLeaf) {
            possibleStrList.add(prefix);
            // 下层
//            queryAllPossibleStr(0, possibleTreeNodeArr[i], possibleTreeNodeArr[i].next, possibleStrList, newPrefix);
        } /*else {
            // 当前无元素,则向右继续找,有则向下和向右找
            if (possibleTreeNodeArr[i] != null) {
                // 下层
                queryAllPossibleStr(0, possibleTreeNodeArr[i], possibleTreeNodeArr[i].next, possibleStrList, newPrefix);
            }
        }*/
        // 当前无元素,则向右继续找,有则向下和向右找
        if (possibleTreeNodeArr[i] != null) {
            // 下层
            queryAllPossibleStr(0, possibleTreeNodeArr[i], possibleTreeNodeArr[i].next, possibleStrList, newPrefix);
        }
        // 不管咋的,都得向右→
        queryAllPossibleStr(i + 1, curNode, possibleTreeNodeArr, possibleStrList, prefix);
    }
}

重点关注两个方法,insertString插入方法,findByPrefix根据前缀获取匹配前缀的字符串列表方法。

测试代码:

java 复制代码
public class Main {
 
	public static void main(String[] args) {
		TrieTree tree=new TrieTree();
		tree.insertString("aba");
		tree.insertString("abc");
		tree.insertString("abcd");
		tree.insertString("jack");
		tree.insertString("amazing");
		tree.insertString("express");
		tree.insertString("engine");
		tree.insertString("engines");
		tree.insertString("equipment");
		tree.insertString("j");
//		tree.insertString("aa");
//		tree.insertString("aa");
//		tree.insertString("ab");
//		tree.insertString("ba");
//		tree.insertString("jack");
//		tree.insertString("jaabcdef");
		//System.out.println(tree.root);
		//System.out.println(tree.size());
		//System.out.println(tree.getStringNum("aa"));
		//System.out.println(tree.getStringNum("ab"));
		//System.out.println(tree.getStringNum("ac"));
		System.out.println(tree.getPrefixNum("a"));
		System.out.println(tree.getPrefixNum("b"));
		System.out.println(tree.getPrefixNum("c"));

		System.out.println(tree.findByPrefix("a"));
		System.out.println(tree.findByPrefix("en"));
	}
 
}

运行:

[aba, amazing, abc, abcd]
[engine, engines]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

3:有啥用

比如你要开发一个自动提示补全的idea插件,就像这样:

或者有其他的功能需要用到类似的功能,都可以考虑使用前缀树。

写在后面

不管是什么技术,只有用到了实际的功能中才算是真正的有用,因此在实际工作中我们要往如何落地应用的方向多考虑。

参考文章列表

Trie树(字典树,前缀树,键树)分析详解

前缀树是什么 前缀树的使用场景

相关推荐
passer__jw76722 分钟前
【LeetCode】【算法】283. 移动零
数据结构·算法·leetcode
爱吃生蚝的于勒1 小时前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~1 小时前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
脉牛杂德2 小时前
多项式加法——C语言
数据结构·c++·算法
一直学习永不止步2 小时前
LeetCode题练习与总结:赎金信--383
java·数据结构·算法·leetcode·字符串·哈希表·计数
wheeldown10 小时前
【数据结构】选择排序
数据结构·算法·排序算法
躺不平的理查德14 小时前
数据结构-链表【chapter1】【c语言版】
c语言·开发语言·数据结构·链表·visual studio
阿洵Rain14 小时前
【C++】哈希
数据结构·c++·算法·list·哈希算法
Leo.yuan15 小时前
39页PDF | 华为数据架构建设交流材料(限免下载)
数据结构·华为
半夜不咋不困15 小时前
单链表OJ题(3):合并两个有序链表、链表分割、链表的回文结构
数据结构·链表