【Leetcode】二十一、前缀树 + 词典中最长的单词

文章目录

  • 1、背景
  • 2、前缀树Trie
  • 3、leetcode208:实现Trie
  • 4、leetcode720:词典中最长的单词

1、背景

在这里插入图片描述
如上,以浏览器搜索时的自动匹配为例:

在这里插入图片描述

如果把所有搜索关键字放一个数组里,则:插入、搜索一个词条时,时间复杂度为O(n),判断某个前缀是否存在,时间复杂度为O(n × m),m为词条长度,因为在遍历数组时,要挨个对比数组每个元素的每个字符和词条前缀的每个字符是否相同,得两层for循环,时间复杂度太高,比如在以下数组判断是否有前缀为haha的关键字:

[goog,googl,google,bai,baidu,gi]

2、前缀树Trie

前缀树,又叫字典树,是一种数据结构,Trie,发音类似 “try”。比如存以下这些数据到前缀树:

goog,googl,google,bai,baidu,gi

效果:

在这里插入图片描述

root节点,一般不存数据,其下有孩子节点。以goog为例,存到第二个g时,这个单词没了,此时,这儿所在的节点,会有一个结束的Flag,以及该Flag处对应的值。从以上的分析,大致可以看出,前缀树Trie这种结构,其对象应该有以下属性:

  • 孩子节点children
  • 某个单词的结束标志isEnd

关于时间复杂度,如果输入字符串str,其长度为k:

  • 插入:O(k)
  • 搜索:O(k)
  • 判断是否存在str这个前缀的词语:O(k)

关于前缀树这种结构的应用场景:

  • 前缀匹配
  • 词频统计(做统计,当然也可用HashMap实现)

3、leetcode208:实现Trie

以英语单词为例,26个字母,根据ASCII码转为数字,就是数组的下标。Trie类应该有个isEnd属性,因为要区分:

  • 是否有str这个单词
  • 是否有以str开头(为前缀)的单词

比较到str的最后一个字母,isEnd为true,说明有str这个单词,是否有这个前缀,则不用考虑isEnd。

此外,正常来说,每个Trie节点的值val也要存一下,但对英文字母不用,因为其对应的SSCII码,可以当下标,下标转一下就是字母值。

在这里插入图片描述

参照以上示意图,每个节点上存着一个字母(索引与ASCII码),写前缀树的实现:

public class Trie {
    
    private Trie[] children;
    private boolean isEnd;

    public Trie() {
        // 26个英文字母,每个节点最多26个儿子节点
        children = new Trie[26];
        isEnd = false;
    }

    public void insert(String word) {
        // 调用insert方法的对象,可认为是根节点
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            // 字母转ASCII码,a对应97,减去a,可让值从0开始,而不是97,方便对应数组下标
            int index = ch - 'a';
            if (node.children[index] == null) {
                // 这是个新字母,创建一个新的节点,作为子节点
                // 这个节点对应的字母的值不用存,下标+97转回去就是这个节点的值
                node.children[index] = new Trie();
            }
            // 该判断word里的下一个字母了,node节点不再是根节点,而是第一个字母的对应的节点
            node = node.children[index];
            
        }
        // 整个word都遍历完了,结束标志为置为true
        node.isEnd = true;
    }

    public boolean search(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            // 字母转ASCII码,a对应97,减去a,可让值从0开始,而不是97,方便对应数组下标
            int index = ch - 'a'; 
            if (node.children[index] == null) {
                // 往下顺,如果有字母不一样,说明一定不存在这个单词
                return false;
            }
            // 检查下一个字母,替换下Tire节点
            node = node.children[index];
        }
        // 和判断前缀是否存在不一样,搜索,找到末尾后,末尾这儿必须有单词的结束标志isEnd
        return node.isEnd;

    }

    public boolean startsWith(String prefix) {
        Trie node = this;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix.charAt(i);
            // 字母转ASCII码,a对应97,减去a,可让值从0开始,而不是97,方便对应数组下标
            int index = ch - 'a';
            if (node.children[index] == null) {
                return false;
            }
            // 检查下一个字母,替换下Tire节点
            node = node.children[index];
        }
        return true;
    }
}

搜索和判断前缀的代码重复度太高,优化下,抽取公共代码

public class Trie {

    private Trie[] children;
    private boolean isEnd;

    public Trie() {
        // 26个英文字母,每个节点最多26个儿子节点
        children = new Trie[26];
        isEnd = false;
    }

    public void insert(String word) {
        // 调用insert方法的对象,可认为是根节点
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            // 字母转ASCII码,a对应97,减去a,可让值从0开始,而不是97,方便对应数组下标
            int index = ch - 'a';
            if (node.children[index] == null) {
                // 这是个新字母,创建一个新的节点,作为子节点
                // 这个节点对应的字母的值不用存,下标+97转回去就是这个节点的值
                node.children[index] = new Trie();
            }
             // 该判断word里的下一个字母了,node节点不再是根节点,而是第一个字母的对应的节点
             node = node.children[index];

        }
        // 整个word都遍历完了,结束标志为置为true
        node.isEnd = true;
    }

    /**
     * 搜索和判断前缀是否存在,两个操作的公共逻辑抽取
     *
     * @param str 输入的字符串
     * @return 返回最后一个字母对应的Trie节点,无则返回null
     */
    public Trie getTrieNode(String str) {
        if (str == null) {
            return null;
        }
        // 调用insert方法的对象,可认为是根节点
        Trie node = this;
        for (int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i);
            // 字母转ASCII码,a对应97,减去a,可让值从0开始,而不是97,方便对应数组下标
            int index = ch - 'a';
            if (node.children[index] == null) {
                // 往下顺,如果有字母不一样,说明一定不存在这个单词或前缀
                return null;
            }
            // 检查str的下一个字母,替换下Tire节点
            node = node.children[index];
        }
        return node;
    }

    public boolean search(String word) {
        Trie trieNode = getTrieNode(word);
        // 和判断前缀是否存在不一样,搜索,找到末尾后,末尾这儿必须有单词的结束标志isEnd
        return trieNode != null && trieNode.isEnd;

    }

    public boolean startsWith(String prefix) {
        return getTrieNode(prefix) != null;
    }
}

从优化后的代码可以看到,搜索和判断前缀的区别是,判断到输入字符的最后一个字母后,搜索要有isEnd标志为true,表示有这样的单词,以免出现,搜abc,但只有abcd时也返回true的情况。而判断前缀是否存在,则不用考虑这个标志位。

4、leetcode720:词典中最长的单词

在这里插入图片描述
如题中示例1,能返回world,需要前面有w ⇒ wo ⇒ wor ⇒ worl这四个词语才行

在这里插入图片描述

将题中数组的每个单词存入前缀树,然后遍历数组。比如app单词,a字母找到了,且isEnd为true,往下ap,也找到了,且isEnd为true,如此app这个单词就是目前符合要求的。

public class P720 {

    public String longestWord(String[] words) {
        if (null == words || words.length == 0) {
            return "";
        }
        Trie trie = new Trie();
        for (String word : words) {
            trie.insert(word);
        }
        String result = "";
        // 控制精确跳到外层循环,而不是内层
        outerLoop:
        for (String word : words) {
            String temp = "";
            for (String s : word.split("")) {
                temp = temp + s;
                if (!trie.search(temp)) {
                    // 如果有一个字母找不到,则直接看题中数组里的下一个单词
                    continue outerLoop;
                }
            }

            // 判断完一个单词符号要求后,如果长度超过了result,则替换
            if (word.length() > result.length()) {
                result = word;
            } else if (word.length() == result.length()) {
                // 如果判断完一个单词符号要求后,如果长度等于result,则对比,取字典序小的
                // compareToIgnoreCase() 方法与 compareTo() 方法类似,但会忽略大小写
                result = word.compareToIgnoreCase(result) < 0 ? word : result;
            }
        }
        return result;
    }
}


以上,套用了208题的Trie类的search方法,search方法只判断搜到末尾时,isEnd是否为true,即它只关心有没有world这个词,而不关心有没有w ⇒ wo ⇒ wor ⇒ worl这四个词语(isEnd为true),再修改下search方法:

public class Trie {

    private Trie[] children;
    private boolean isEnd;

	//略,同上一题
	/**
     * 搜索是否有word单词,以及w ⇒ wo ⇒ wor ⇒ worl这四个单词
     */
    public boolean searchByStep(String word) {
        if (word == null) {
            return false;
        }
        // 根节点
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';
            // 没有这个字母,或者这地方结束标志为false,则返回false
            if (node.children[index] == null || !node.children[index].isEnd) {
                return false;
            }
            // 检查str的下一个字母,替换下Tire节点
            node = node.children[index];
        }
        // 到最后一个字母所在的节点了
        return node != null && node.isEnd;
    }
}

用新的前缀树搜索方法(判断word是否存在的同时,还要判断w ⇒ wo ⇒ wor ⇒ worl这四个是否存在),并简化下实现代码:

public class P720 {
    
    public String longestWord(String[] words) {
        if (null == words || words.length == 0) {
            return "";
        }
        Trie trie = new Trie();
        for (String word : words) {
            trie.insert(word);
        }
        String result = "";
        for (String word : words) {
            // 不符合条件,判断下一个单词
            if (!trie.searchByStep(word)) {
                continue;
            }
            // 判断完一个单词符合要求后,如果长度超过了result,则替换
            // 如果判断完一个单词符号要求后,如果长度等于result,则对比,取字典序小的替换result
            // compareToIgnoreCase() 方法与 compareTo() 方法类似,但会忽略大小写
            if (word.length() > result.length() || (word.length() == result.length()) && word.compareToIgnoreCase(result) < 0) {
                result = word;
            } 
        }
        return result;
    }
}



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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024 HNCTF PWN(close ezpwn idea what beauty)

文章目录 closeezpwn代码利用exp idea代码exp whatexp beauty libc 2.35IDA中文乱码解决代码思路exp close int __fastcall main(int argc, const char **argv, const char **envp) {puts("**********************************");puts("* Welcome to the H…

什么是页分裂?insert 操作对 B+ 树结构的改变是什么样的?

什么是页分裂&#xff1f; 如果我们使用非自增主键&#xff0c;由于每次插入主键的索引值都是随机的&#xff08;比如 UUID&#xff09;&#xff0c;因此每次插入新的数据时&#xff0c;就可能会插入到现有数据页中间的某个位置&#xff0c;这将不得不移动其它数据来满足新数据…

浅谈Visual Studio 2022

Visual Studio 2022&#xff08;VS2022&#xff09;提供了众多强大的功能和改进&#xff0c;旨在提高开发者的效率和体验。以下是一些关键功能的概述&#xff1a;12 64位支持&#xff1a;VS2022的64位版本不再受内存限制困扰&#xff0c;主devenv.exe进程不再局限于4GB&#xf…

安防视频监控/视频汇聚EasyCVR平台浏览器http可以播放,https不能播放,如何解决?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台基于云边端一体化架构&#xff0c;兼容性强、支持多协议接入&#xff0c;包括国标GB/T 28181协议、部标JT808、GA/T 1400协议、RTMP、RTSP/Onvif协议、海康Ehome、海康SDK、大华SDK、华为SDK、宇视SDK、乐橙SDK、萤石云SD…

[图解]企业应用架构模式2024新译本讲解27-层超类型3

1 00:00:01,020 --> 00:00:04,340 下一个就是更新家属数量 2 00:00:04,830 --> 00:00:09,140 它又找了一个ID为2的&#xff0c;拿出来 3 00:00:09,150 --> 00:00:09,800 然后更新 4 00:00:10,300 --> 00:00:11,770 没有什么新东西&#xff0c;一样的 5 00:00:1…

netxduo http server 创建回复以及json解析

我们今天要整http的response,比如我创建的http server,我对它发送了一个POST,然后服务器解析出json里的body,再回复过去。今天会用到json的解析库cjson以及postman去发送消息。这次用nx_web_http_server.h这个库,不用之前的nx_http_server.h 本教程在最后附带app_netxduo…

java通过jwt生成Token

定义 JWT&#xff08;JSON Web Token&#xff09;简而言之&#xff0c;JWT是一个加密的字符串&#xff0c;JWT传输的信息经过了数字签名&#xff0c;因此传输的信息可以被验证和信任。一般被用来在身份提供者和服务提供者间传递被认证用户的身份信息&#xff0c;以便于从资源服…

Flutter TextFiled频繁采集“剪切板信息”

在使用Flutter开发者&#xff0c;输入框是必不可少的功能&#xff0c;最近产品出了需要&#xff0c;要求输入框记住用户登录过的手机号&#xff0c;并在输入框输入时提示出来&#xff0c;这是个很基础的功能&#xff0c;但是在通过测试验收发布到应用市场时&#xff0c;被Vivo拒…

基于springboot和mybatis的RealWorld后端项目实战三之添加swagger

pom.xml添加依赖 <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><arti…

keepalived+haproxy实现nginx高可用

1.需求&#xff1a; 在之前我们使用的keepalivednginx的方案&#xff0c;架构如下&#xff1a; 该方案的缺点是资源使用率不高&#xff0c;只能在吞吐量不高的场景使用 第二种方案&#xff1a;haproxynginx&#xff0c;架构图如下&#xff1a; 这个会有单点故障&#xff0c;当…

鸿蒙语言基础类库:【@system.geolocation (地理位置)】

地理位置 说明&#xff1a; 从API Version 7 开始&#xff0c;该接口不再维护&#xff0c;推荐使用新接口[ohos.geolocation]。本模块首批接口从API version 3开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import geolocation from …

css - - - - - 环形倒计时进度条实现

css - - - - - 环形倒计时进度条实现 1. 效果图展示2. 代码展示 1. 效果图展示 2. 代码展示 // html <view class"father"><view class"progress" style"--progress:{{red}}; --last:{{gray}}"></view> </view>// css …

SQL每日一题:查找重复的电子邮箱

题干 表: Person -------------------- | Column Name | Type | -------------------- | id | int | | email | varchar | -------------------- id 是该表的主键&#xff08;具有唯一值的列&#xff09;。 此表的每一行都包含一封电子邮件。电子邮件不包含大写字母。 编写解决…

鸿蒙语言基础类库:【@system.file (文件存储)】

文件存储 说明&#xff1a; 从API Version 6开始&#xff0c;该接口不再维护&#xff0c;推荐使用新接口[ohos.fileio]。本模块首批接口从API version 3开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import file from system.file;f…

一套完整的养老院人员定位解决方案包含哪些内容?

养老院人员定位解决方案是建立面向社区及养老组织的传感网系统与信息渠道&#xff0c;并在此基础上提供实时、方便、高效、低成本的、物联化、互联化、智能化的养老服务。 人口老龄化问题早已成为当今社会关注的重要问题之一。在养老院封闭的环境&#xff0c;养老院希望利用智…

【数据结构】探索排序的奥秘

若有不懂地方&#xff0c;可查阅我之前文章哦&#xff01; 个人主页&#xff1a;小八哥向前冲~_csdn博客 所属专栏&#xff1a;数据结构_专栏 目录 排序的概念 几种排序方法介绍 冒泡排序 选择排序 插入排序 堆排序 向上调整建堆排序 向下调整建堆排序 希尔排序 快速…

sentinel网关限流配置及使用

sentinel控制台源码&#xff1a;https://download.csdn.net/download/yixin605691235/89543923 sentinel控制台jar包&#xff1a;https://download.csdn.net/download/yixin605691235/89543931 不同环境直接修改jar包中的application.yml文件中的nacos地址就可以了。 一、网关限…

算法2--贪心算法

1.老鼠和猫的交易 小老鼠准备了M磅的猫粮&#xff0c;准备去和看守仓库的猫做交易&#xff0c;因为仓库里有小老鼠喜欢吃的五香豆。 仓库有N个房间&#xff1b; 第i个房间有 J[i] 磅的五香豆&#xff0c;并且需要用 F[i] 磅的猫粮去交换&#xff1b; 老鼠不必交换该房间所有的五…

MySQL(3)表的操作

目录 1. 表的操作; 2. 数据类型; 1. 表的操作: 1.1 创建表: 语法: create table 表名( 属性 类型 [comment ], 属性 类型 [comment ], 属性 类型 ) character set 字符集 collate 校验集 engine 存储引擎; 前面博客提到: MyISAM和InoDB这两个比较重要. 1.2 查看表…

3. 序列生成

1.复习状态机&#xff0c;使用状态机实现序列生成 1.1 设计要求 用有限状态机生成序列001011&#xff0c;串行循环输出该序列。 1.2 设计代码&#xff0c;仿真及波形 状态机&#xff1a;不考虑状态简化的情况下&#xff0c;要输出的序列多少位&#xff0c;就用多少个状态&a…