【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都遍历完了,结束标志为置为truenode.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];}// 和判断前缀是否存在不一样,搜索,找到末尾后,末尾这儿必须有单词的结束标志isEndreturn 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都遍历完了,结束标志为置为truenode.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);// 和判断前缀是否存在不一样,搜索,找到末尾后,末尾这儿必须有单词的结束标志isEndreturn 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,则返回falseif (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.mzph.cn/news/872292.shtml

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

相关文章

SEO:6个避免被搜索引擎惩罚的策略-华媒舍

在当今数字时代&#xff0c;搜索引擎成为了绝大多数人获取信息和产品的首选工具。为了在搜索结果中获得良好的排名&#xff0c;许多网站采用了各种优化策略。有些策略可能会适得其反&#xff0c;引发搜索引擎的惩罚。以下是彭博社发稿推广的6个避免被搜索引擎惩罚的策略。 1. 内…

一文带你看懂SAP-HANA的基本架构与原理

注&#xff1a;本篇主要对SAP HANA做了总结与论述&#xff0c;如有错误欢迎读者提出并补充 创作不易,希望大家一键三连支持!!!♥♥♥ 创作不易,希望大家一键三连支持!!!♥♥♥ 创作不易,希望大家一键三连支持!!!♥♥♥ 目录 一. 背景引入1.1 硬件与数据库系统1.2 行业现状 …

AES Android IOS H5 加密方案

前景&#xff1a; 1、本项目原有功能RSA客户端对敏感信息进行加密 2、本次漏洞说是服务端返回值有敏感信息&#xff0c;需要密文返回 方案&#xff1a; 本次方案不算完美&#xff0c;还是有被劫持篡改的风险&#xff0c;但基本https证书认证加持&#xff0c;风险相对较小 …

Camera XTS 处理笔记

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 常用测试步骤&#xff08;下面均以CTS为例&#xff09; 打开终端&#xff0c;进入 cts 包 tools目录下执行 ./cts-tradefed 进入cts测试 :~/XTS/CTS/14…

永磁同步电机高性能控制算法(14)—— 有源阻尼电流环

1.前言 在之前的之后中已经发过一篇复矢量电流环和我们平时用的比较多的前馈补偿的电流环的对比&#xff0c;感觉复矢量电流环的效果还是挺明显的。 https://zhuanlan.zhihu.com/p/682880365https://zhuanlan.zhihu.com/p/682880365 当时在看文献的时候&#xff0c;复矢量电…

AI算法17-贝叶斯岭回归算法Bayesian Ridge Regression | BRR

贝叶斯岭回归算法简介 贝叶斯岭回归&#xff08;Bayesian Ridge Regression&#xff09;是一种回归分析方法&#xff0c;它结合了岭回归&#xff08;Ridge Regression&#xff09;的正则化特性和贝叶斯统计的推断能力。这种方法在处理具有大量特征的数据集时特别有用&#xff…

13、Shell自动化运维编程基础

弋.目录 RHCE板块一、为什么学习和使用Shell编程二、Shell是什么1、shell起源2、查看当前系统支持的shell3、查看当前系统默认shell4、Shell 概念 三、Shell 程序设计语言1、Shell 也是一种脚本语言2、用途 四、如何学好shell1、熟练掌握shell编程基础知识2、建议 五、Shell脚本…

英伟达股票1拆10后,现在再买入是否为时已晚?

英伟达股票1拆10后&#xff0c;现在再买入是否为时已晚&#xff1f; 英伟达的股价在过去18个月里已经上涨了近800% 人工智能领域无疑是当下最受投资者关注的焦点之一&#xff0c;而这一领域的佼佼者--英伟达&#xff0c;也被一些华尔街投资机构和看好半导体、数据中心行业的专业…

SoulApp创始人张璐团队以AI驱动社交进化,平台社交玩法大变革

在科技飞速发展的今天,人工智能正逐步渗透到社交媒体的各个环节,赋能全链路社交体验。AI的引入不仅提升了内容推荐的精准度,使用户能够更快速地发现感兴趣的内容,还能通过用户行为预测,帮助平台更好地理解和满足用户需求。此外,AI驱动的虚拟助手和聊天机器人也正在改变用户互动…

NVIDIA RTX 50系显卡接口全变,功耗爆炸超500W

七月伊始&#xff0c;手机圈就开始打的不可开交了。 例如真我 GT6、IQOO Neo 9S、以及蓄势待发的红米 K70 Ultra&#xff0c;都想在这个暑假向莘莘学子发出最诚挚的「邀请函」。 反观电脑圈这边&#xff0c;不能说一潭死水&#xff0c;只能说毫无波澜。 不过该来的还是要来的&…

Redis的使用(四)常见使用场景-缓存使用技巧

1.绪论 redis本质上就是一个缓存框架&#xff0c;所以我们需要研究如何使用redis来缓存数据&#xff0c;并且如何解决缓存中的常见问题&#xff0c;缓存穿透&#xff0c;缓存击穿&#xff0c;缓存雪崩&#xff0c;以及如何来解决缓存一致性问题。 2.缓存的优缺点 2.1 缓存的…

睿考网:造价员和造价工程师是一个意思吗?

在工程建设领域中&#xff0c;经常会有人问&#xff1a;“造价员和造价工程师是一样的吗?”这两者代表的是两种独立的职业身份&#xff0c;职责和资格要求有明显的差异&#xff0c;是两种完全不同的考试。 造价工程师是一种具有专业资质的人员&#xff0c;通过国家统一的执业…

『 Linux 』命名管道

文章目录 命名管道与匿名管道命名管道特点命名管道的理解命名管道实现两个毫无关联的进程间通信 命名管道与匿名管道 命名管道是管道的一种,数据流向为单向故被称为管道; 与匿名管道相同属于一种内存级文件; 区别如下: 名字 匿名管道 没有名字,只存在于内存当中(类似内核缓冲…

【软件测试】编写测试用例篇

前面部分主要是编写测试用例的方法和方向&#xff0c;后面一部分是编写出具体的测试用例 目录 什么是测试用例 1.设计测试用例的万能公式 1.1.从思维出发 1.2.万能公式 1.3.弱网测试 1.4.安装与卸载测试 2.设计测试用例的方法 2.1.基于需求的设计方法 2.2.等价类 2.3…

测试开发面经总结(三)

TCP三次握手 TCP 是面向连接的协议&#xff0c;所以使用 TCP 前必须先建立连接&#xff0c;而建立连接是通过三次握手来进行的。 一开始&#xff0c;客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口&#xff0c;处于 LISTEN 状态 客户端会随机初始化序号&…

原来,BI数据分析也是有模板的

在当今数据驱动的时代&#xff0c;商业智能&#xff08;BI&#xff09;数据分析已经成为企业决策的重要工具。然而&#xff0c;很多人可能并不了解&#xff0c;BI数据分析并非从零开始&#xff0c;而是可以依托现成的模板和解决方案来快速搭建和实施的。以奥威BI方案为例&#…

React+TS前台项目实战(二十九)-- 首页构建之性能优化实现首页Echarts模块数据渲染

文章目录 前言Echart模块源码功能分析数据渲染一、HashRateEchart统计图1. 功能分析2. 代码详细注释 二、BlockTimeChart统计图1. 功能分析2. 代码详细注释 三、使用方式四. 数据渲染后效果如下 总结 前言 还记得之前我们创建的 高性能可配置Echarts组件 吗&#xff1f;今天我…

redis 配置文件参数详解

1、redis.conf 通用类 Redis的配置文件是一个文本文件&#xff0c;通常名为redis.conf。以下是一些常见配置项的解释和示例&#xff1a; 1、bind 127.0.0.1&#xff1a;绑定的主机地址 2、 protected-mode ,默认是开启状态&#xff0c;一般不需要修改&#xff0c;可以保证服务…

唯众物联网综合实训台 物联网实验室建设方案

物联网综合实训装置 物联网工程应用综合实训台是我公司针对职业院校物联网行业综合技能型人才培养&#xff0c;综合运用传感器技术、RFID技术、接口控制技术、无线传感网技术、Android应用开发等&#xff0c;配合实训台上的433M无线通信设备、ZigBee节点、射频设备、控制设备、…

智能家居产品公司网站源码,自适应布局设计,带完整演示数据

适合各类智能家居电子产品使用的网站源码&#xff0c;深色大气设计&#xff0c;自适应布局设计&#xff0c;pc手机均可完美适配&#xff0c;带完整演示数据。 独家原创资源。源码是asp开发的&#xff0c;数据库是access&#xff0c;主流的虚拟主机空间都支持asp&#xff0c;直…