面试经典150题——串联所有单词的子串(困难)

news2024/11/28 14:51:12

"Opportunities don't happen, you create them." ​

- Chris Grosser

gray concrete road between brown and green leaf trees at daytime

1. 题目描述

2.  题目分析与解析

2.1 思路一——暴力求解

遇见这种可能刚开始没什么思路的问题,先试着按照人的思维来求解该题目。对于一个人来讲,我想要找到 s 字符串中包含 words 中所有字符串以任意顺序排列连接起来的子串的开始索引,那么我可能会有这种思路:

// 初始化wordsAndCount
for (String word : words) {
    wordsAndCount.put(word, wordsAndCount.getOrDefault(word, 0) + 1);
}

  • 遍历字符串 s

  • 对于每一个字符开头的与words中字符串长度相同的子串,尝试在words中匹配

    • 如果匹配成功,就需要将words中当前字符串删除,下一次就从当前字符 + words[i].length处开始继续匹配,以此循环,直到words为空,记录起始下标。

    • 如果匹配失败,从下一个字符处继续重新匹配。

  • 同时需要注意由于words中可能有相同的单词,因此我们需要记录如果是相同单词其出现的次数,这样就方便在遍历时删除,只需要将对应的值减一,当减到0就从words中删除该单词即可。初始化可以按照如下方式:

但是显然这种方法非常的耗时,因为对于每一个S字符串的字符你都需要查找是否匹配,最坏的情况下需要走完words的总长度,因此总遍历为S的长度 N 乘以 words的长度 M。但是起码我们能够解决问题,这能给我们一点自信再去想怎么优化,所以遇到问题先想想人怎么解决,没有思路的情况下就先不要太在意时间复杂度。

我按照如下的代码运行如预期会超时:

2.2 思路二——滑动窗口

对于这种一块一块的内容去对比的问题,我们应该敏锐的去想能不能用滑动窗口解决。刚好我在这也稍微总结一下什么情况下可以使用滑动窗口来解决,这样我们遇到新的题目就能更快速的想到可能的解决办法,再去尝试。

滑动窗口思想运用场景

滑动窗口技术是一种用于解决数组或列表相关问题的算法策略,特别适用于需要处理连续数据段的问题。这种技术广泛应用于各种场景,尤其是在需要优化时间和空间复杂度的场合。

  1. 最大/最小子数组问题:当你需要找到数组中某个大小的连续子数组,使得其和最大或最小时,滑动窗口技术可以有效地解决问题。

  2. 固定大小的问题:对于需要处理固定大小窗口的问题,如计算给定大小的窗口内的平均值、最大值或最小值,滑动窗口是一个理想的解决方案。

  3. 可变大小的问题:对于窗口大小不固定的问题,如找到数组中和至少为K的最短子数组,滑动窗口技术可以通过动态调整窗口大小来寻找最优解。

  4. 字符串匹配问题:在处理字符串时,如查找字符串中包含所有字符的最短子串,滑动窗口能够有效地跟踪字符出现的情况。

  5. 计数问题:当需要计数或统计特定条件下的连续子数组(或子序列)数量时,滑动窗口可以在遍历数组的同时维护这些统计信息。

  6. 双指针问题:在一些双指针问题中,滑动窗口可以视为一种特殊的双指针实现,尤其是当问题涉及到连续序列或子数组时。

  7. 连续序列问题:需要找到满足特定条件的连续序列时,如和为特定值的连续正数序列,滑动窗口提供了一种高效的解决方案。

滑动窗口技术之所以强大,是因为它能够在遍历数据的同时,通过调整窗口的大小或位置来动态地聚焦于问题的特定部分,从而在保持算法效率的同时减少不必要的重复计算。在解决这类问题时,滑动窗口不仅能够提供优化的解决方案,还能帮助理解和分析数据的连续性质。

回到题目

根据我的理解,其实滑动窗口的核心思想

  • 就是把已经遍历过的内容存储在窗口里,这样对于下一次的遍历能够重复使用,减少再次遍历的开销。

而如果我们明确了使用滑动窗口来解决题目,那么我们就需要从以下几点着手:

  1. 窗口的大小:我们想把窗口指定多大比较合适?

  2. 窗口的移动:窗口可以向前“滑动”,每次移动可以是一步也可以是多步,这取决于问题的具体要求。窗口的移动使得算法能够逐步遍历整个数据集。我们应该怎么设置窗口的移动规则?

  3. 数据的复用:当窗口移动时,一些数据会从窗口中移出,同时会有新的数据加入到窗口中。窗口内的这些数据可以被复用来计算新的结果,从而避免了对已经遍历过的数据进行重复的计算。那我们怎么在这个问题中复用数据?

  4. 动态调整:窗口的大小是否需要动态调整?对于某些问题,窗口的大小可以根据特定的条件动态调整,比如在寻找满足条件的最小子序列长度时。这种动态调整能够帮助算法更加灵活地应对不同的问题需求。

根据上面几个点,我们可以一一来回答,但首先我们先回过头看看暴力解法为什么那么慢?

原因就是我每一次从一个字符开始,明明后面的很多内容遍历过了,但是在第二次for循环时我们还需要从第二个字符开始,重新走之前走的内容。因此我们就可以想一想是不是就可以指定一个窗口,用来容纳那些已经走过的地方,等到下一次遍历,我就直接使用这个窗口的数据。

但是这个窗口存什么呢?

窗口中存储的就是我们上一次for循环走过的尝试与words中内容进行匹配的数据,匹配不成功自然是从下一个位置开始,但是如果匹配成功了,那么下一次遍历我们就可以不需要再次匹配窗口中的数据

因此就可以回答上面提出的几点问题了:窗口大小动态变化,窗口的移动一次移动一个words[i]的长度,但是这样还不够,因为如果每次跳过words[i].length个字符的话,我们对于每一个words[i]长度内部的起始位置我们会忽视掉,因为正如前几篇滑动窗口讲得一样,虽然滑动窗口很高效,但是它并没有偷懒,也就是它并没有忽视对每个位置的判断,只是将判断的过程简化了。如果我们以words[i]的长度为步长,会得到下图:

就是一个一个红色的框,但是我们忽视了如下紫色和绿色的框的起始位置的判断:

但是幸运的是,只需要words[i]个长度的框,就会出现重复的框,如下:

虽然可能解释起来用话说并不是特别清晰,但是通过图片我想大家应该知道是什么意思。因此我们的窗口就可以分为words[i].length类,比如 words = ["ab","cd","ef"],因为words[i].length = 2,所以定义2个窗口,就可以覆盖所有的范围。

接下来我们来考虑窗口如何利用数据:

如上图所示,对于字符串S遍历过程中,指定三个窗口,如果能够匹配成功words中的某一个单词,就扩充窗口,这样在下一次遍历到窗口一类型的起始点的时候,如上紫色框所示内容就已经知道是能够匹配成功的,就无需再进行匹配,只需要判断黄色框是否能匹配即可。

因此我们基本可以得到如下的代码思路:(定义 words[i].length = n

  1. 初始化,定义 n 个窗口,同时定义 n 个hashMap,用来存储words中的单词及其出现的个数(也可以放在每一次的外层for循环中)

  2. 外层for循环遍历窗口的个数(n个)

  3. 内层循环遍历不同的开头,比如对于窗口一,其遍历起始位置集合为:{ 0, 0 + n, 0 + 2n ...... }。移动右指针,每次加入一个单词,对应的一个记录窗口内部的单词计数的hashMap对应位置也加一。

    • 如果当前单词对应窗口内部计数开始大于 存储words中的单词及其出现的个数,说明已经出现匹配不成功的情况了,因此就需要移动左指针,直到删除这个单词。

    • 如果窗口内单词的出现次数,也就是 right-left的长度等于 words 中的单词的总长,说明找到了一个解。因为我们先对窗口内部的单词进行了计数,如果大于存储words中的单词及其出现的个数是会移动左指针的,而当任意一个窗口内部的单词的数量小于存储words中的单词数量,那长度肯定是不匹配的,所以只有窗口内部每一个的单词的数量完全匹配words中的单词数量,也就是 right-left的长度等于 words 中的单词的总长,说明匹配成功!

3. 代码实现

3.1 暴力求解

运行结果如上所示会超时!

3.2 滑动窗口

4. 相关复杂度分析

为了分析这两种解法的时间和空间复杂度,我们假定一些基本参数:

  • 假设字符串s的长度为N

  • 假设单词数组words包含M个单词,每个单词的长度为L

  • 因此,所有单词串联形成的字符串长度是M*L

4.1 暴力解法的复杂度分析

时间复杂度

  • 对于字符串s中的每个字符,算法尝试匹配所有单词串联形成的子串。

  • 对于每个起始位置,算法最坏情况下需要比较M*L长度的字符串。

  • 因此,最坏情况下的时间复杂度是O(N*M*L)

空间复杂度

  • 需要一个HashMap来存储words中单词及其出现的次数,其空间复杂度为O(M)

  • 每次循环时,都会创建一个wordsAndCount的副本,因此空间复杂度保持为O(M)

4.2 滑动窗口解法的复杂度分析

时间复杂度

  • 由于窗口的滑动是线性的,并且每个字符最多被访问两次(一次加入窗口,一次从窗口移除),这个复杂度可以优化为O(N)

空间复杂度

  • 使用了两个HashMap来存储words中单词及其出现次数以及当前窗口内单词及其出现次数,空间复杂度为O(M)

4.3 总结

  • 暴力解法的时间复杂度较高,为O(N*M*L),空间复杂度为O(M)

  • 滑动窗口解法通过优化遍历过程,将时间复杂度降低到O(N),空间复杂度保持为O(M)

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

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

相关文章

Java的Cloneable接口和深拷贝

Java 中内置了一些很有用的接口, Clonable 就是其中之一。 Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 "拷贝"。 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常。 浅拷贝&#xff…

数据结构-并查集

并查集原理 在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个 单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询一 个元素归属于那个集合的运算。适合于描述这类…

cool Nodejs后端框架 如何快速入门 写一个接口

1.cool 框架 js前端开发者 想自己写后端接口 快速入门的就是node.js 了 可以用这个框架自己做一些东西 或者实现前后端的开发 2.目录结构 这个基本上 就是cool 框架的项目结构 主要是 这个src 中的modules 文件夹 这个文件夹 主要是一些接口模块 比如 business 中 相当于…

leetcode 448. 找到所有数组中消失的数字

用的最土的办法&#xff0c;将数组nums中出现过的数字用map记录下来&#xff0c;再遍历1~n中的所有数字&#xff0c;凡是未在map中出现过的即为我们要找的数字。 Java代码如下&#xff1a; class Solution {public List<Integer> findDisappearedNumbers(int[] nums) {i…

品牌之门:概率与潜力的无限延伸

在品牌的世界里&#xff0c;每一个成功的推广都像是打开一扇门&#xff0c;从未知走向已知&#xff0c;从潜在走向显现。这扇门&#xff0c;既是品牌的起点&#xff0c;也是品牌发展的无限可能。 品牌&#xff0c;就像一扇紧闭的门&#xff0c;它静静地矗立在那里&#xff0c;…

优先级队列(堆)_PriorityQueue

前言 想要看如何使用可以通过目录跳转到 PriorityQueue的使用 优先级队列 概念 队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队 列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场…

linuxqq关闭主面板后无法再次打开的问题

文章目录 前言解决方案强调一点 前言 听说QQ出了linux版&#xff0c;所以来试试。结果试试就逝世。这次记录一个关闭后没办法打开的解决办法。 解决方案 刚安装好后如果点了关闭&#xff0c;系统托盘里也没有&#xff0c;点击图标又是重新登录。当然&#xff0c;我们最简单、…

反序列化漏洞(一)Shiro漏洞CVE-2016-4437复现

★★免责声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将信息做其他用途&#xff0c;由Ta承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 1、前言 春节后第一篇&#xff0c;祝大家龙年一切顺利&…

书生浦语大模型实战营-课程笔记(1)

模型应用过程&#xff0c;大致还是了解的。和之前实习做CV项目的时候比起来&#xff0c;多了智能体这个环节。智能体是个啥&#xff1f; 类似上张图&#xff0c;智能体不太清楚。感觉是偏应用而不是模型的东西&#xff1f; 数据集类型很多&#xff0c;有文本/图片/视频。所以…

仰暮计划|“​他们艰苦半生,但真的希望祖国安祥,山河无恙”

自述&#xff0c;自赎 我没有在那个年代生活过&#xff0c;我一出生就是盛世中国&#xff0c;看遍了祖国的大好河山。但我没想到&#xff0c;走了这么远的路&#xff0c;吃了这么多的苦的爷爷会一直跟我说“不是国家不好&#xff0c;只是中国的钱拿去还债了&#xff0c;过了那…

什么是 Docker 容器?以及操作 Docker 容器相关的命令汇总

镜像仓库常用指令&#xff1a;Docker 镜像仓库是什么&#xff1f;有哪些镜像仓库命令&#xff1f; 镜像常用指令&#xff1a;操作 Docker 镜像的常用命令 1. 什么是容器&#xff1f; 容器是镜像的运行实体。容器是基于镜像创建的可运行实例&#xff0c;并且单独存在&#xff0…

FastAI 之书(面向程序员的 FastAI)(八)

原文&#xff1a;www.bookstack.cn/read/th-fastai-book 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第二十章&#xff1a;总结思考 原文&#xff1a;www.bookstack.cn/read/th-fastai-book/cedc7ab42349d210.md 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA…

Github 2024-02-14 开源项目日报 Top9

根据Github Trendings的统计&#xff0c;今日(2024-02-14统计)共有9个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Rust项目4TypeScript项目1PowerShell项目1Java项目1JavaScript项目1Jupyter Notebook项目1非开发语言项目1Pyth…

每日五道java面试题之java基础篇(八)

第一题.CopyOnWriteArrayList的底层原理是怎样的 ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的&#xff0c;在向CopyOnWriteArrayList添加元素时&#xff0c;会复制⼀个新的数组&#xff0c;写操作在新数组上进⾏&#xff0c;读操作在原数组上进⾏并且&#xff0c;写操作…

Hive调优——合并小文件

目录 一、小文件产生的原因 二、小文件的危害 三、小文件的解决方案 3.1 小文件的预防 3.1.1 减少Map数量 3.1.2 减少Reduce的数量 3.2 已存在的小文件合并 3.2.1 方式一&#xff1a;insert overwrite (推荐) 3.2.2 方式二&#xff1a;concatenate 3.2.3 方式三&#xff…

DOM事件练习1

DOM事件练习1 1. 演示效果 2. 代码分析 用 ul 创建四个 li 列表整个列表的背景是红色的&#xff0c;鼠标悬浮在列表上&#xff0c;一行的变为蓝色点击任意列表&#xff0c;整个列表的背景变为白色&#xff0c;被点击的列表变为粉色需要用到 js 的点击事onclick件和forEach循环…

手撕Promise

文章目录 一、Promise的初体验1.初体验——抽奖案例 二、Promise的实践练习1.实践练习——fs读取文件2.实践练习——AJAX请求 三、Promise的常见骚操作1.封装fs读取文件操作2.util.promisify方法进行promise风格转化3.封装原生的Ajax4.Promise实例对象的两个属性&#xff08;1&…

《Think in Java》

《Think in Java》 第一章&#xff1a;对象导论 1.1 抽象过程 1&#xff09;万物皆对象。 2&#xff09;程序是对象的集合&#xff0c;它们通过发送消息来告诉彼此所要做的。 3&#xff09;每个对象都有其他对象构成的存储&#xff0c;一个对象可以复用其他对象&#xff0c;从而…

Sentinel 流控-关联模式

关联模式 A关联B, 当B流控后,A 的流控规则也生效了 条件 A 设置高级流控规则,关联 B资源B 设置普通流控规则(独立规则)实例 接口编写 package com.learning.springcloud.order.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.s…

论文解读:MobileOne: An Improved One millisecond Mobile Backbone

论文创新点汇总&#xff1a;人工智能论文通用创新点(持续更新中...)-CSDN博客 论文总结 关于如何提升模型速度&#xff0c;当今学术界的研究往往聚焦于如何将FLOPs或者参数量的降低&#xff0c;而作者认为应该是减少分支数和选择高效的网络结构。 概述 MobileOne(≈MobileN…