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

相关文章

  • Elasticsearch percolate 查询示例详解

    Elasticsearch percolate 查询示例详解

    这篇文章主要为大家介绍了Elasticsearch percolate 查询示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • Java定时任务Timer、TimerTask与ScheduledThreadPoolExecutor详解

    Java定时任务Timer、TimerTask与ScheduledThreadPoolExecutor详解

    这篇文章主要介绍了Java定时任务Timer、TimerTask与ScheduledThreadPoolExecutor详解,  定时任务就是在指定时间执行程序,或周期性执行计划任务,Java中实现定时任务的方法有很多,本文从从JDK自带的一些方法来实现定时任务的需求,需要的朋友可以参考下
    2024-01-01
  • java 和 json 对象间转换

    java 和 json 对象间转换

    这篇文章主要介绍了java 和 json 对象间转换,需要的朋友可以参考下
    2014-03-03
  • Java Collections.sort()实现List排序的默认方法和自定义方法

    Java Collections.sort()实现List排序的默认方法和自定义方法

    这篇文章主要介绍了Java Collections.sort()实现List排序的默认方法和自定义方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2017-06-06
  • Spring MVC请求处理流程和九大组件详解

    Spring MVC请求处理流程和九大组件详解

    这篇文章主要介绍了Spring MVC请求处理流程和九大组件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-12-12
  • java递归实现复制一个文件夹下所有文件功能

    java递归实现复制一个文件夹下所有文件功能

    这篇文章主要介绍了java递归实现复制一个文件夹下所有文件功能n次,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-08-08
  • java中使用Files.readLines()处理文本中行数据方式

    java中使用Files.readLines()处理文本中行数据方式

    这篇文章主要介绍了java中使用Files.readLines()处理文本中行数据方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • JAVA中使用MD5加密实现密码加密

    JAVA中使用MD5加密实现密码加密

    本篇文章主要介绍JAVA中使用MD5加密实现密码加密,很多地方都要存储用户密码,这里整理了详细的代码,有需要的小伙伴可以参考下
    2017-07-07
  • Java如何实现海量数据判重

    Java如何实现海量数据判重

    在海量数据如何确定一个值是否存在?这是一道非常经典的面试场景题,那怎么回答这个问题呢?下面小编就来和大家详细的聊一聊,感兴趣的可以一起学习一下
    2023-09-09
  • 在java中使用dom解析xml的示例分析

    在java中使用dom解析xml的示例分析

    本篇文章介绍了,在java中使用dom解析xml的示例分析。需要的朋友参考下
    2013-05-05

最新评论