详解Java前缀树Trie的原理及代码实现

 更新时间:2022年11月18日 08:56:58   作者:刘Java  
Trie又被称为前缀树、字典树。Trie利用字符串的公共前缀来高效地存储和检索字符串数据集中的关键词,最大限度地减少无谓的字符串比较,其核心思想是用空间换时间。本文主要介绍了Trie的原理及实现,感兴趣的可以了解一下

Trie的概念

Trie(发音类似 “try”)又被称为前缀树、字典树。Trie利用字符串的公共前缀来高效地存储和检索字符串数据集中的关键词,最大限度地减少无谓的字符串比较,其核心思想是用空间换时间。

Trie树可被用来实现字符串查询、前缀查询、词频统计、自动拼写、补完检查等等功能。

Trie树的三个性质:

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

Trie的实现

Trie是一颗非典型的多叉树形结构,多叉树是因为一个节点可以有多个字节点,而非典型在于节点中没有专门的字段直接保存此节点的值,而是通过一个数组或者map保存了当前节点的所有下层子节点的值,也正是因此,根节点不表示任何字符。

基本结构

最简单的前缀树的结构如下:

  • 内部包含一个哈希表next,存储着子节点的值到对应的节点的映射关系。
  • 还有一个布尔值isEnd,用来标识该节点是否是一个字符串的结束。
  • 调用无参构造器将会初始化这两个属性。

实际上,还可以包含其他的属性以实现特定的功能,例如加入count表示以当前单词结尾的单词数量,加入prefix表示以该处节点之前的字符串为前缀的单词数量。

另外,对于下层子节点的存储,如果字符串仅包含小写字母,或者固定范围的字符,那么我们可以使用定长(例如26)的数组来表示next,这样省去了hash操作的开销,但同样可能造成空间的浪费。

class Trie {
    /**
* 经过该节点的字符串的下层节点
*/
    Map<Character, Trie> next;
    /**
* 该节点是否是一个字符串的结束
*/
    boolean isEnd;

    public Trie() {
        this.next = new HashMap<>();
        this.isEnd = false;
    }

}

构建Trie

通过调用构造器初始化一个Trie的根节点,通过insert操作向前缀树中插入关键词字符串(模式串)。

可以看到其实现的方法比较简单:将字符串转换为char数组,顺序遍历char数组的每个字符,然后从根节点开始判断该节点的下层子节点映射next:

  • 如果不包含此字符,那么加入一个新子节点进去,值对应着当前字符。然后使用该子节点,进入下一次循环判断下一个字符。
  • 如果包含此字符或者新插入了节点,那么当前字符获取对应的子节点,进入下一次循环判断下一个字符。

循环完毕,我们完成了当前字符串的Trie构建,那么还需要将最后一个节点的isEnd改为true,表示该节点是一个字符串的结束。

public void insert(String word) {
    //初始默认为根节点,根节点不包含任何字符
    Trie cur = this;
    //遍历该字符串的字符数组
    for (char c : word.toCharArray()) {
        //如果该节点的下层不包含此字符,那么加入一个新节点进去
        if (!cur.next.containsKey(c)) {
            cur.next.put(c, new Trie());
        }
        //查找下一层节点
        cur = cur.next.get(c);
    }
    //遍历字符串完毕,最后的节点isEnd置为true,表示一个字符串的结束
    cur.isEnd = true;
}

查找字符串

基于Trie结构可以查找字符串、匹配前缀、查找出现次数等等,这里我们给出查找字符串和查找前缀的方法。

比较简单,我们从字典树的根开始,查找前缀:

  • 如果子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
  • 如果子节点不存在。说明字典树中不包含该前缀,返回空指针。

可以看到查找字符串相比于匹配前缀,仅仅是多了一个最下层子节点是否是一个字符串的结束的判断而已。

/**
 * 查找字符串
 */
public boolean search(String word) {
    Trie end = searchPrefix(word);
    return end != null && end.isEnd;
}

/**
 * 匹配前缀
 */
public boolean startsWith(String prefix) {
    return searchPrefix(prefix) != null;
}

private Trie searchPrefix(String prefix) {
    //初始默认为根节点,根节点不包含任何字符
    Trie cur = this;
    //遍历该字符串的字符数组
    for (char c : prefix.toCharArray()) {
        //如果该节点的下层不包含此字符,那么直接返回null
        if (!cur.next.containsKey(c)) {
            return null;
        }
        //查找下一层节点
        cur = cur.next.get(c);
    }
    return cur;
}

Trie的总结

假设我们加入了she、he、his、her这四个字符串,那么Trie的结构如下,其中红色节点表示其属于某个字符串的尾部节点。

Trie时间复杂度:初始化为O(1),每次操作为O(N),N为插入或查找的字符串的长度。

Trie空间复杂度:O(N),N表示Trie结点数量,或者说所有插入字符串的长度之和(减去相同前缀长度)。如果是采用定长数组表示next,那么空间复杂度为O(N*M),M表示字符集的大小,即数组长度。

可以看到,使用Trie的数据结构使得插入、查询全词、查询前缀的时间复杂度与已插入的单词数目和长度无关,这是它的一个优点。

但是,Trie又名前缀树,因为它只能基于前缀匹配实现某些功能。另一些功能,例如判断一段字符串中是否包含某些关键词,不需要前缀匹配,此时就无法使用Trie了。

相关题目如下:208. 实现 Trie (前缀树)

完整实现如下:

class Trie {
    /**
     * 经过该节点的字符串的下层节点
     */
    Map<Character, Trie> next;
    /**
     * 该节点是否是一个字符串的结束
     */
    boolean isEnd;

    public Trie() {
        this.next = new HashMap<>();
        this.isEnd = false;
    }

    public void insert(String word) {
        //初始默认为根节点,根节点不包含任何字符
        Trie cur = this;
        //遍历该字符串的字符数组
        for (char c : word.toCharArray()) {
            //如果该节点的下层不包含此字符,那么加入一个新节点进去
            if (!cur.next.containsKey(c)) {
                cur.next.put(c, new Trie());
            }
            //查找下一层节点
            cur = cur.next.get(c);
        }
        //遍历字符串完毕,最后的节点isEnd置为true,表示一个字符串的结束
        cur.isEnd = true;
    }

    /**
     * 查找字符串
     */
    public boolean search(String word) {
        Trie end = searchPrefix(word);
        return end != null && end.isEnd;
    }

    /**
     * 匹配前缀
     */
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix) != null;
    }

    private Trie searchPrefix(String prefix) {
        //初始默认为根节点,根节点不包含任何字符
        Trie cur = this;
        //遍历该字符串的字符数组
        for (char c : prefix.toCharArray()) {
            //如果该节点的下层不包含此字符,那么直接返回null
            if (!cur.next.containsKey(c)) {
                return null;
            }
            //查找下一层节点
            cur = cur.next.get(c);
        }
        return cur;
    }

    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insert("你是xxl吗?");
    }
}

以上就是详解Java前缀树Trie的原理及代码实现的详细内容,更多关于Java前缀树Trie的资料请关注脚本之家其它相关文章!

相关文章

  • spring中的ObjectPostProcessor详解

    spring中的ObjectPostProcessor详解

    这篇文章主要介绍了spring中的ObjectPostProcessor详解,Spring Security 的 Java 配置不会公开其配置的每个对象的每个属性,这简化了大多数用户的配置,毕竟,如果每个属性都公开,用户可以使用标准 bean 配置,需要的朋友可以参考下
    2024-01-01
  • java使用poi导出Excel的方法

    java使用poi导出Excel的方法

    这篇文章主要为大家详细介绍了java使用poi导出Excel的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08
  • 出现次数超过一半(50%)的数

    出现次数超过一半(50%)的数

    给出n个数,需要我们找出出现次数超过一半的数,下面小编给大家分享下我的实现思路及关键代码,感兴趣的朋友一起学习吧
    2016-07-07
  • 带你入门Java的数组

    带你入门Java的数组

    这篇文章主要给大家介绍了关于Java中数组的定义和使用的相关资料,,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-07-07
  • Java关键字final的实现原理分析

    Java关键字final的实现原理分析

    这篇文章主要介绍了Java关键字final的实现原理分析,在JDK8之前,如果在匿名内部类中需要访问局部变量,那么这个局部变量一定是final修饰的,但final关键字可以省略,需要的朋友可以参考下
    2024-01-01
  • springboot 按月分表的实现方式

    springboot 按月分表的实现方式

    本文主要介绍了springboot 按月分表的实现方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • JVM内存模型/内存空间:运行时数据区

    JVM内存模型/内存空间:运行时数据区

    这篇文章主要介绍了JVM内存模型/内存空间的相关资料,帮助大家更好的理解和学习Java虚拟机,感兴趣的朋友可以了解详细,希望能够给你带来帮助
    2021-08-08
  • Java 如何利用缓冲流读写文件

    Java 如何利用缓冲流读写文件

    这篇文章主要介绍了Java 如何利用缓冲流读写文件的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • jar包和war包区别解析

    jar包和war包区别解析

    jar是java普通项目打包,通常是开发时要引用通用类,打成jar包便于存放管理,war是java web项目打包,web网站完成后,打成war包部署到服务器,目的是为了节省资源,提供效率,这篇文章主要介绍了jar包和war包区别及理解,需要的朋友可以参考下
    2023-07-07
  • 详解SpringBoot配置连接池

    详解SpringBoot配置连接池

    本篇文章主要详解SpringBoot配置连接池,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-04-04

最新评论