前缀树介绍,定义,图文详解分析——Java/Kotlin双版本代码

news2024/11/25 8:19:38

前缀树

前缀树,又称作字典树,用一个树状的数据结构储存字典中的所有单词。

列,一个包含can、cat、come、do、i、in、inn的前缀树如下图所示:

在这里插入图片描述

  • 前缀树是一个多叉树,一个节点可能存在多个节点。
  • 除根节点外,每个节点表示一个字符串的一个字符,整个字符串由前缀树的路径进行表示。
  • 字符串在前缀树的路径并不一定全部都是终止于叶节点,比如in对应的则是字符串inn的一部分。

前缀树的实现

接下来设计一颗前缀树PrefixTree,其包含如下的操作。

  1. insert,在前缀树中添加一个字符串。
  2. search,查找,如果该前缀树中存在该字符串,则返回true
  3. startWith,查找字符串前缀,如果前缀树中存在包含该字符串的前缀,则返回true,否则返回false
  4. matchingStartWith,查找所有包含该前缀的字符串,返回前缀树中所有包含该前缀的字符串,如果不存在则返回null

首先定义前缀树的数据结构,注意这里我们只考虑实现小写英文字母(具体可以根据业务逻辑调整),因为小写英文字母只有26个,所以我们可以将其放到一个容量为26的数组之中去。数组中第一个元素则是对应a的节点,第二个元素则是对应b的节点,依次往下。需要说明的是,不需要专门一个字段表示当前节点的字符,因为可以根据其在父节点对应的位置,从而得知对应的字符信息。

另外我们需要一个boolean类型的字段,去判断该节点的路径对应的字符串是否是一个完整的字符。

综上,数据结构可以定义为如下所示:

kotlin

class PrefixTree {
    private val root = Node()
    class Node {
        val childNode = Array<Node?>(26) { null }
        var isWord = false
    }
}

java

    class PrefixTree {
        private Node root =new Node();
        class Node {
            Node[] childNode = new Node[26];
            boolean isWord = false;
        }
    }

insert函数实现

我们添加boybosshiboyhi四个单词,具体分析一下前缀树添加的过程:

  1. 添加boy,逐个添加字母boy对应的节点,并将最后一个字母y对应的节点的isWord标记为true
  2. 添加boss,此时前两个字母bo对应的节点之前已经创建出来,因此只需要创建两个对应s的节点,并将第2个s对应节点的字段isWord标记为true
  3. 添加hiboy,虽然之前有添加过boy三个单词,但是其并不是hiboy的前缀,所以需要为hiboy每个单词创建节点,并将最后的节点置为true
  4. 添加hi,由于hi为前缀,所以不需要创建节点添加,只需要将i对应的节点的isWord置为ture即可

在这里插入图片描述

所以,插入的代码可以做如下的实现

kotlin

    fun insert(word: String) {
        var localRoot = root
        word.toCharArray().forEach { char ->
            val index = char - 'a'
            if (localRoot.childNode[index] == null) {
                localRoot.childNode[index] = Node()
            }
            localRoot = localRoot.childNode[index]!!
        }
        localRoot.isWord = true
    }

java

        public void insert(String world){
            Node localRoot = root;
            for (char c :world.toCharArray()){
                int index = c - 'a';
                if (localRoot.childNode[index]==null){
                    localRoot.childNode[index] = new Node();
                }
                localRoot = localRoot.childNode[index]
            }
            localRoot.isWord = true;

        }

search函数实现

这个思考起来比较简单一点,直接从根节点开始查找,如果要查找的字符串对应的字符在根节点中不存在,则直接返回false,如果存在则前往该节点,在该节点查找是否存在与字符串对应的第二个字符的节点,没有返回false,如果存在则继续往下查找,直到查找完最后一个字符,最终判断如果isWordtrue则返回true,否则则返回false

所以代码如下所示:

kotlin

    fun search(word: String): Boolean {
        var localRoot = root
        word.toCharArray().forEach { char: Char ->
            val index = char - 'a'
            if (localRoot.childNode[index] != null) {
                localRoot = localRoot.childNode[index]!!
            } else {
                return false
            }
        }
        return localRoot.isWord
    }

java

        public boolean search(String world){
            Node localRoot = root;
            for (char c :world.toCharArray()){
                int index = c - 'a';
                if (localRoot.childNode[index]!=null){
                    localRoot = localRoot.childNode[index];
                }else {
                    return false;
                }
            }
            return localRoot.isWord;

        }

startWith函数实现

函数startWithsearch不同,只要前缀树中存在以输入的前缀开头的单词就应该返回true。因此,函数startWith的返回值和函数search不同之外,其他代码是一样的。
kotlin

    fun startWith(word: String): Boolean {
        var localRoot = root
        word.toCharArray().forEach { char: Char ->
            val index = char - 'a'
            if (localRoot.childNode[index] != null) {
                localRoot = localRoot.childNode[index]!!
            } else {
                return false
            }
        }
        return true
    }

java

        public boolean startWith(String world){
            Node localRoot = root;
            for (char c :world.toCharArray()){
                int index = c - 'a';
                if (localRoot.childNode[index]!=null){
                    localRoot = localRoot.childNode[index];
                }else {
                    return false;
                }
            }
            return true;

        }

matchingStartWith函数实现

该函数要求返回所有匹配到前缀的单词。

所以分为以下两个步骤:

  1. 匹配前缀,如果匹配不到直接返回null
  2. 匹配到之后,在该前缀的节点进行深度优先搜索,将遍历到的所有单词全部加入集合进行返回

所以代码如下所示:
kotlin

    fun matchingStartWith(word: String): List<String>? {
        val wordResultPre = StringBuilder()
        var localRoot = root
        word.toCharArray().forEach { char: Char ->
            val index = char - 'a'
            if (localRoot.childNode[index] != null) {
                wordResultPre.append(char)
                localRoot = localRoot.childNode[index]!!
            } else {
                return null
            }
        }
        //深度优先搜索 所有的剩余单词
        val result = ArrayList<String>()
        dfs(localRoot, StringBuilder(),wordResultPre,result)
        return result
    }

    private fun dfs(node: Node, str: StringBuilder, wordResultPre: StringBuilder, result: ArrayList<String>) {
        val childNodes = node.childNode
        for (index in childNodes.indices) {
            val childNode = childNodes[index]
            if (childNode != null) {
                str.append(('a' + index).toChar())
                if (childNode.isWord) {
                    result.add(wordResultPre.toString() + str.toString())
                }
                dfs(childNode,str, wordResultPre, result)
            }
        }
    }

java

        public List<String> matchingStartWith(String world){
            StringBuilder wordResultPre = new StringBuilder();
            Node localRoot = root;
            for (char c :world.toCharArray()){
                int index = c - 'a';
                if (localRoot.childNode[index]!=null){
                    wordResultPre.append(c);
                    localRoot = localRoot.childNode[index];
                }else {
                    return null;
                }
            }
            //深度优先搜索 所有的剩余单词
            List<String> result =new ArrayList<>();
            dfs(localRoot,new StringBuilder(),wordResultPre,result);
            return result;

        }
        private void dfs(Node node, StringBuilder stringBuilder, StringBuilder wordResultPre, List<String> result) {
            Node[] childNodes = node.childNode;
            for (int i =0 ;i<childNodes.length;i++){
                Node childNode = childNodes[i];
                if (childNode!=null){
                    stringBuilder.append('a'+i);
                    if (childNode.isWord){
                        result.add(wordResultPre.toString()+stringBuilder.toString());
                    }
                    dfs(childNode,stringBuilder,wordResultPre,result);
                }
            }
        }

前缀树应用

题目

输入一个包含n个单词的数组,可以把它们编码成一个字符串和n个下标。例如,单词数组["time","me","bell"]可以编码成一个字符串"time#bell#",然后这些单词就可以通过下标[0,2,5]得到。对于每个下标,都可以从编码得到的字符串中相应的位置开始扫描,直到遇到’#'字符前所经过的子字符串为单词数组中的一个单词。所以如果输入的是字符串数组["time","me","bell"],那么编码之后最短的字符串是"time#bell#",长度是10

分析

题目的目标是得到最短的编码,因此,如果一个单词是另一个单词的后缀,那么单词在编码字符串中就不需要单独出现,这是因为单词可以通过在单词中偏移下标得到。

所以我们很容易想到前缀树,但是题目是关于字符串的后缀。所以,我们可以反转字符串,之后使用前缀树。

根据题目信息,我们不需要关心每个单词,只需要遍历每个叶子节点对应的字符串的长度,然后加在一起即可。但是注意,字符串和字符串之间存在#,所以需要对长度进行额外+1。

总体来说使用深度优先搜索,代码参考如下:

    public void minLength(String [] words){
        PrefixTree.Node node = build(words);
        int [] total  = {0};
        dfs(node,1,total);
    }
    
    private void dfs(PrefixTree.Node node, int length, int[] total) {
        boolean isLeaf = true;
        for (PrefixTree.Node childNode : node.getChildNode()) {
            if (childNode != null) {
                isLeaf = false;
                dfs(childNode, length + 1, total);
            }
        }
        if (isLeaf) {
            total[0] += length;
        }
    }

🙆‍♀️。欢迎技术探讨噢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/99219.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring5框架总结学习(从入门到进阶)

文章目录Spring51、如何创建一个Spring项目&#xff08;idea版&#xff09;2、 IOC容器1、XML解析工厂模式反射2、bean管理1、总述2、基于XML创建对象3、基于XML注入属性4、基于XML注入属性&#xff0c;属性值为空或特殊符号5、基于XML注入属性&#xff0c;外部bean6、基于XML注…

【第十二章 MVCC(多版本并发控制),隐藏字段,undolog(版本链),readview,原理分析(RC,RR)】

第十二章 MVCC&#xff08;多版本并发控制&#xff09;&#xff0c;隐藏字段&#xff0c;undolog&#xff08;版本链&#xff09;&#xff0c;readview&#xff0c;原理分析&#xff08;RC&#xff0c;RR&#xff09; 1.基本概念: &#xff08;1&#xff09; 当前读&#xff1…

hevc 基于均值的RDO模式删减

1 在获得粗略模式候选列表L4之后&#xff0c;对尺寸为4x4和8x8的PU保留8种预测模式&#xff0c; 对尺寸16x16&#xff0c;32x32, 64x64的PU保留3种预测模式&#xff0c;如果可以跳过部分模式的RDO计算&#xff0c;则可以进一步减少编码时间。 2 由于HCost计算是RDO计算的一种较…

springBoot+Cache(自定义有效时间配置)

一、背景 sprinbBoot的cache是不是支持动态设置缓存注解的&#xff0c;因此本次自己实现一个可以动态设置缓存时间的配置。 源码&#xff1a;示例地址 二、步骤 1、pom.xml添加依赖配置 <dependency><groupId>org.springframework.boot</groupId><arti…

Linux系统编程(续)

静态库制作及使用步骤&#xff1a; 1.将.c生成.o文件 gcc -c add.c -o add.o 2.使用ar工具制作静态库 ar rcs lib自定义库名.a 后面需要的.c文件 3.编译静态库到可执行文件中 gcc test.c 自制的库.a -o test 注意:如果程序中没有函数声明&#xff0c;编译器会自动给个隐式声明…

Java+MySQL基于SSM的在线投票系统

随着社会的发展,人们在处理一些问题的时候不同意见越来越多,这源于人们对思想的解放和对社会的认识。所以在处理同一问题上,为了征求不同人的意见在线投票系统诞生了。 基于SSM的在线投票系统以钦州学院为背景,运用在校所学习的软件开发原理,采用Spring&#xff1a;SpringMVC&a…

如何在网页画一个旋转的粉色圣诞树(含源代码)

0 效果 做法&#xff1a; 创建三个文件tree.html、tree.js、tree.css&#xff0c;放在同一个目录下按1、2、3小节填充这三个文件浏览器打开tree.html文件 1 .HTML-基本布局 搞一个.html文件&#xff0c;内容如下 <!DOCTYPE html> <html lang"en"><…

【OpenCV-Python】教程:7-1 理解 kNN (k-Nearest Neighbour)

OpenCV Python 理解kNN &#xff08;k-Nearest Neighbour&#xff09; 【目标】 理解 kNN 算法的基本概念 【理论】 kNN是监督学习中最简单的分类算法之一。其思想是在特征空间中搜索与测试数据最接近的匹配。我们将用下图来研究它。 在图像中&#xff0c;有两个"家族…

一文看懂 InnoDB 的内存淘汰逻辑(LRU)

InnoDB淘汰的逻辑是怎样的呢&#xff1f; InnoDB 内存管理用的是最近最少使用 (Least Recently Used, LRU) 算法&#xff0c;这个算法的核心就是淘汰最久未使用的数据。 下图是一个 LRU 算法的基本模型。 InnoDB 管理 Buffer Pool 的 LRU 算法&#xff0c;是用链表来实现的。…

Go项目实战:02-微服务micro services

1、微服务&#xff08;micro services&#xff09; 单体式架构服务 过往大家熟悉的服务器。 特性&#xff1a; 1、复杂性随着开发越来越高&#xff0c;遇到问题解决困难。2、技术债务逐渐上升。3、耦合度高&#xff0c;维护成本大。 - 1、出现bug&#xff0c;不容易排查 - 2…

[ 数据结构 -- 手撕排序算法第六篇 ] 快速排序

文章目录前言一、常见的排序算法二、快速排序的基本思想三、快速排序的不同实现1.hoare版本2. 挖坑法3. 前后指针法4.三种版本单趟排序结果5.快速排序三数取中优化6.小区间优化四、快速排序的特性总结前言 手撕排序算法第六篇&#xff1a;快速排序&#xff01; 从本篇文章开始…

JavaSE面试题(二)

1&#xff1a;说一说八大基本数据类型 2&#xff1a;面向对象 面向对象的核心&#xff0c;就是类和对象。Java中的面向对象的思想&#xff1a;万物皆对象。 类&#xff1a;是对一类事物的描述&#xff0c;是抽象的&#xff0c;看不见&#xff0c;摸不着。 对象&#xff1a;是实…

week 7 吴恩达 调参 ,batch norm,softmax

文章目录前言7.1调整参数的过程 Turing progress7.2、scale7.3 如果在实践中探寻超参数7.4 batch normalization 批量归一化7.5 将BN算法拟合到神经网络中7.6 为什么 BN有效&#xff1f;7.7测试时的BN7.8 7.9 softmax regression7.10深度学习的框架前言 7.1调整参数的过程 Turi…

CentOS 8:环境变量

环境变量 环境变量&#xff0c;就是放在当前环境中的变量 无论Linux &#xff0c;还是Windows&#xff0c;都有环境变量 比如&#xff0c;最常用的环境变量 PATH, JAVA_HOME 定义环境变量 export JAVA_HOME/opt/jdk1.8 显示环境变量 echo $JAVA_HOME 查看所有环境变量…

c语言位操作和变量存储类型

c语言位操作 c语言变量存储类型 格式[存储类型说明符] 数据类型说明符 变量名&#xff0c;例如&#xff0c;auto int a;但一般情况下auto是省略的 其他类型说明符还有&#xff1a;static 、extern、register auto最普通动态存储&#xff0c;但所在范围的函数程序结束后&#xf…

处理模型视图中的选择

有关在视图中选择的项的信息存储在QItemSelectionModel类中&#xff0c;这将维护单个模型中项的模型索引&#xff0c;并且独立于任何视图。由于一个模型可以有多个视图&#xff0c;因此可以在视图之间共享选择&#xff0c;从而允许应用程序以一致的方式显示多个视图 选择由选…

Redis高并发锁(二)乐观锁

文章目录redis乐观锁1. watch 监控key2. multi 开启事务3. exec 执行事务4. 演示1) 先用两个连接AB访问redis2) A监控key,此时库存是45013) A开启事务&#xff0c;并且将库存-1,事务进入队列等待执行4&#xff09;此时B更新库存为20015&#xff09;A开始执行事务业务改造1. Sto…

C++11--包装器与可变参数摸板

文章目录可变参数模板递归函数方式展开参数包函数包装器举个例子bind函数举个例子可变参数模板 C11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板 // Args是一个模板参数包&#xff0c;args是一个函数形参参数包 // 声明一个参数包Args... args&…

Spring Cloud之Feign消费和Hystrix熔断

Spring Cloud的Feign消费和Hystrix熔断 现如今&#xff0c;由于互联网的流行&#xff0c;很多特产都可以在网上订购&#xff0c;你可以在堆满积雪的冬北订购海南的椰子&#xff0c;海南的椰子就会采用很快的物流方式调送到堆满积地的东北&#xff0c;就相当于在本地实现了买椰…

Opencv(C++)笔记--图像金字塔

目录 1--图像金字塔的原理 2--图像金字塔的用途 3--Opencv API 3-1--拉普拉斯金字塔上采样 3-2--高斯金字塔下采样 3-3--代码实例 4--参考 1--图像金字塔的原理 图像金字塔常用于图像缩放&#xff08;resize&#xff09;和图像分割当中&#xff0c;不同分辨率的图像以金…