详解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的资料请关注脚本之家其它相关文章!

相关文章

  • java实现扫雷游戏入门程序

    java实现扫雷游戏入门程序

    这篇文章主要为大家详细介绍了java实现扫雷游戏入门程序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-06-06
  • MybatisPlus调用原生SQL的三种方法实例详解

    MybatisPlus调用原生SQL的三种方法实例详解

    这篇文章主要介绍了MybatisPlus调用原生SQL的三种方法,在有些情况下需要用到MybatisPlus查询原生SQL,MybatisPlus其实带有运行原生SQL的方法,我这里列举三种,需要的朋友可以参考下
    2022-09-09
  • java连接zookeeper实现zookeeper教程

    java连接zookeeper实现zookeeper教程

    这篇文章主要介绍了java连接zookeeper实现zookeeper教程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • SpringBoot整合RabbitMQ及生产全场景高级特性实战

    SpringBoot整合RabbitMQ及生产全场景高级特性实战

    本文主要介绍了SpringBoot整合RabbitMQ及生产全场景高级特性实战,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-10-10
  • 区分Java的方法覆盖与变量覆盖

    区分Java的方法覆盖与变量覆盖

    作为初学者2个比较容易出错的定义,方法覆盖和变量覆盖。下面我们一起来看看作者如何去探讨Java的方法覆盖和变量覆盖。
    2015-09-09
  • Java语言Iterator转换成 List的方法

    Java语言Iterator转换成 List的方法

    在 Java 中,迭代器(Iterator)是一种用于遍历集合中元素的对象,它提供了一种简单而一致的方式来访问集合中的元素,而不需要暴露集合内部的结构,这篇文章主要介绍了Java语言Iterator转换成 List的方法,需要的朋友可以参考下
    2023-08-08
  • 浅谈Springboot整合RocketMQ使用心得

    浅谈Springboot整合RocketMQ使用心得

    本篇文章主要介绍了Springboot整合RocketMQ使用心得,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • Spring的BeanFactoryPostProcessor接口示例代码详解

    Spring的BeanFactoryPostProcessor接口示例代码详解

    这篇文章主要介绍了Spring的BeanFactoryPostProcessor接口,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-02-02
  • Java中的观察者模式实例讲解

    Java中的观察者模式实例讲解

    这篇文章主要介绍了Java中的观察者模式实例讲解,本文先是讲解了观察者模式的概念,然后以实例讲解观察者模式的实现,以及给出了UML图,需要的朋友可以参考下
    2014-12-12
  • Java实现Excel转PDF的两种方法详解

    Java实现Excel转PDF的两种方法详解

    使用具将Excel转为PDF的方法有很多,在这里我给大家介绍两种常用的方法:使用spire转化PDF、使用jacob实现Excel转PDF,分别应对两种不一样的使用场景,需要的可以参考一下
    2022-01-01

最新评论