【面试经典 150 | 滑动窗口】串联所有单词的子串

news2025/4/16 14:49:50

文章目录

  • 写在前面
  • Tag
  • 题目来源
  • 题目解读
  • 解题思路
    • 方法一:两个哈希表
    • 方法二:滑动窗口
  • 写在最后

写在前面

本专栏专注于分析与讲解【面试经典150】算法,两到三天更新一篇文章,欢迎催更……

专栏内容以分析题目为主,并附带一些对于本题涉及到的数据结构等内容进行回顾与总结,文章结构大致如下,部分内容会有增删:

  • Tag:介绍本题牵涉到的知识点、数据结构;
  • 题目来源:贴上题目的链接,方便大家查找题目并完成练习;
  • 题目解读:复述题目(确保自己真的理解题目意思),并强调一些题目重点信息;
  • 解题思路:介绍一些解题思路,每种解题思路包括思路讲解、实现代码以及复杂度分析;
  • 知识回忆:针对今天介绍的题目中的重点内容、数据结构进行回顾总结。

Tag

【滑动窗口】【字符串】


题目来源

面试经典 150 | 30. 串联所有单词的子串


题目解读

找出字符串数组 words 中字符串按任意顺序组成的新字符串在字符串 s 中的开始索引,以数组的形式返回。其中,words 中所有字符串的长度都相同。


解题思路

首先,我们记 sLen 为字符串 s 的长度,wsLen 为字符串数组 words 的长度,wLen 为字符串数组中每个字符串的长度。

本题的解题思路其实一目了然:判断 s 中长度为 wsLen * wLen 的子串是否可以由 words 中字符串按任意顺序组合而成(下文以匹配代之),如果可以的话,那么长度为 wsLen * wLens 子串的开始索引就是有效的答案,加入答案数组 res 即可。

本题关键就是如何判断是否 “匹配”。

方法一:两个哈希表

我们使用两个哈希表来辅助 “匹配”。

第一个哈希表 mp1 用来记录 words 中的字符串出现的次数,第二个哈希表 mp2 用来记录当前长度为 wsLen * wLen 的子串中长度为 wLen 的字符串出现次数,第二个哈希表更新结果如下图所示,其中字符串 s = barfoochewsLen = 1wLen = 3

如果 mp2 中有任何一个字符串出现的次数大于在 mp1 中出现的次数,或者mp2 中有一个字符串没有在 mp1 中出现过,则匹配失败。

具体实现中,我们可以边更新 mp2,边匹配:

  • 迭代长度为 wsLen * wLens 子串中的所有字符串进行,记当前的字符串为 ss
  • 首先判断 ss 是否在 mp1 中,如果不在,当前长度为 wsLen * wLens 子串一定不匹配;
  • 如果在,则更新 mp2,接着判断 mp2[ss]mp1[ss] 的大小关系,如果前者大,则当前长度为 wsLen * wLens 子串一定不匹配。
  • 如果迭代完长度为 wsLen * wLens 子串中的所有字符串都没有出现以上不匹配的情况,则说明长度为 wsLen * wLens 子串的开始索引就是有效的答案,加入答案数组 res 即可。

但是,实际测试,方法一超时。我觉得问题在于【先枚举滑窗再枚举单词数】,如果像答案那样【先枚举单词数,再跳跃枚举滑窗】

还有【一次哈希】的解法,不论是一次哈希还是两次哈希最坏的时间复杂度都达到 了 1 0 8 10^8 108,是这样计算的 1 0 4 × 1 0 3 × 30 = 1 0 8 10^4 \times 10^3 \times 30 = 10^8 104×103×30=108,官方题解的时间复杂度为 1 0 3 × 1 0 4 ÷ 30 × 30 = 1 0 7 10^3 \times 10^4 \div 30 \times 30 = 10^7 103×104÷30×30=107,就差那一点最后两个测试用例就没通过。

该方法超时了,实现代码 就不贴出来了。

方法二:滑动窗口

此时利用的是 438. 找到字符串中所有字母异位词 方法二中的优化版的滑动窗口来解决,可以参考 滑窗 differ 优化 进行理解。

其实方法一也是利用的滑动窗口,其中的长度为 wsLen * wLens 子串就是一个固定的窗口下的子串,不同于方法一【先枚举所有的滑窗再枚举单词数】,方法二的滑窗是【先枚举单词数,再跳跃枚举滑窗】,现在来具体看一看是如何实现的。

首先需要将 s 划分为单词组,每个单词的大小均为 wLen,一共有 wLen 中划分方式。如下图所示为 s = "barfooche"words = ["bar", "foo"] 对于 s 的三种划分方式。

(1)
(2)
(3)

划分成单词组后,一个滑窗包含 s 中前 wsLen 个单词,用一个哈希表 differ 来表示窗口内单词频次和 words 中单词频次之差。初始化 differ 时,出现在窗口中的单词,每出现一次,相应的值增加 1,出现在 words 中的单词,每出现一次,相应的值减少 1。示例代码:

// 每一种分组方式的 differ 初始化
for (int i = 0; i < n && i + m * n <= ls; ++i) {
    unordered_map<string, int> differ;
		// 每一种分组方式的窗口内 differ ++
    for (int j = 0; j < m; ++j) {
        ++differ[s.substr(i + j * n, n)];
    }
		// 每一种分组方式 words 内 differ --
    for (string &word: words) {
        if (--differ[word] == 0) {
            differ.erase(word);
        }
    }
}

然后将窗口右移,右侧会加入一个单词,左侧会移出一个单词,并对 differ 做相应的更新。窗口移动时,若出现 differ 中值不为 0 的键的数量为 0,则表示这个窗口中的单词频次和 words 中单词频次相同,窗口的左端点是一个待求的起始位置。示例代码:

for (int start = i; start < ls - m * n + 1; start += n) {
		// 以初始化的 differ 为基础进行判断
    if (start != i) {       // start = i 时,直接判断 differ 是否为空,为空则 start = i 是一个起始位置
		    // 先加入一个单词
        string word = s.substr(start + (m - 1) * n, n);
        if (++differ[word] == 0) {
            differ.erase(word);
        }
				// 后移除一个单词
        word = s.substr(start - n, n);
        if (--differ[word] == 0) {
            differ.erase(word);
        }
    }
		// 如果 differ 为空,则表示这个窗口中的单词频次和 `words` 中单词频次相同,将当前起始位置加入答案数组
    if (differ.empty()) {
        res.emplace_back(start);
    }
}

总的实现代码

class Solution {
public:
    vector<int> findSubstring(string &s, vector<string> &words) {
        vector<int> res;
        int m = words.size(), n = words[0].size(), ls = s.size();
        for (int i = 0; i < n && i + m * n <= ls; ++i) {
            unordered_map<string, int> differ;
            for (int j = 0; j < m; ++j) {
                ++differ[s.substr(i + j * n, n)];
            }
            for (string &word: words) {
                if (--differ[word] == 0) {
                    differ.erase(word);
                }
            }
            for (int start = i; start < ls - m * n + 1; start += n) {
                if (start != i) {
                    string word = s.substr(start + (m - 1) * n, n);
                    if (++differ[word] == 0) {
                        differ.erase(word);
                    }
                    word = s.substr(start - n, n);
                    if (--differ[word] == 0) {
                        differ.erase(word);
                    }
                }
                if (differ.empty()) {
                    res.emplace_back(start);
                }
            }
        }
        return res;
    }
};

复杂度分析

时间复杂度: O ( n × m ) O(n \times m) O(n×m) n n nwords 中每个单词的长度, m m ms 的长度。

空间复杂度: O ( n × k ) O(n \times k) O(n×k) n n nwords 中每个单词的长度, k k kwords 的长度,每次滑动窗口时,需要用一个哈希表保存单词频次。


写在最后

如果文章内容有任何错误或者您对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家有更优的时间、空间复杂度方法,欢迎评论区交流。

最后,感谢您的阅读,如果感到有所收获的话可以给博主点一个 👍 哦。

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

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

相关文章

【图论C++】树的重心——教父POJ 3107(链式前向星的使用)

》》》算法竞赛 /*** file * author jUicE_g2R(qq:3406291309)————彬(bin-必应)* 一个某双流一大学通信与信息专业大二在读 * * brief 一直在竞赛算法学习的路上* * copyright 2023.9* COPYRIGHT 原创技术笔记&#xff1a;转载…

$nextTick解决echarts宽度固定为100%的问题

问题描述:vue+element项目中使用到了tab切换选项卡,其中有一个tab下的内容是echarts,出现了echarts宽度缩减为100px无法继承100%属性。 问题: echarts渲染时tab选项卡display为none,所以width:100%没有可继承项,被echarts自带方法切割成100px。 我他喵的解决这个问题还是…

给 shell 自定义快捷键

shell 快捷键原理 本质上对于 shell 来说只有输入输出&#xff0c;它不会监听你系统快捷键的&#xff0c;监听快捷键其实是终端模拟器的责任&#xff0c;他会将你输入的快捷键转换成字符串序列。当我们使用了类似 ctrl -> 这样的快捷键&#xff0c;shell 会根据你输入的字…

QT的ui设计中改变样式表的用法

在QT的ui设计中,我们右键会弹出一个改变样式表的选项,很多人不知道这个是干什么的。 首先我们来看下具体的界面 首先我们说一下这个功能具体是干嘛的, 我们在设置很多控件在界面上之后,常常都是使用系统默认的样式,但是当有些时候为了美化界面我们需要对一些控件进行美化…

进阶JS-内置构造函数

基本数据类型&#xff1a;string、number、boolean、undefined、null 引用类型:对象 其实字符串、数值、布尔等基本类型也都有专门的构造函数&#xff0c;这些我们称为包装类型。 JS中几乎所有的数据都可以基于构成函数创建。 const str andy//其实是const strnew String(a…

【EI会议征稿】第三届机械、建模与材料工程国际学术会议(I3ME 2023)

第三届机械、建模与材料工程国际学术会议&#xff08;I3ME 2023&#xff09; 2023 3rd International Conference on Mechanical, Modeling and Materials Engineering 第三届机械、建模与材料工程国际学术会议&#xff08;I3ME 2023&#xff09;将于2023年12月1-3日在中国长春…

一套成熟的Spring Cloud智慧工地平台源码,自主版权,开箱即用

智慧工地云平台源码 APP端PC端数据大屏端全套源码 自主版权&#xff0c;开箱即可项目使用 智慧工地利用移动互联、物联网、云计算、大数据等新一代信息技术&#xff0c;彻底改变传统施工现场各参建方的交互方式、工作方式和管理模式&#xff0c;为建设集团、施工企业、监理单位…

第二证券:券商近期关注点浮现 扎堆调研19只个股 上调旅游股评级

19只个股 被券商扎堆调研 东方财富Choice数据显现&#xff0c;9月以来券商保持高强度的调研节奏&#xff0c;19只个股获得超25家券商扎堆调研&#xff0c;首要会合在半导体、医药生物、新能源、机械设备等范畴。人气最高的股票分别是埃斯顿、周大生、南网科技、卫星化学、宁波…

11.1Spring基础(核心概念,创建和使用,简单读取)

一.Spring概念: 1.Spring就是包含了众多工具方法的IoC容器. 2.IoC容器:控制反转,指的是对象的生命周期,将对象的生命周期(什么时候创建,销毁)交给Spring进行管理. 在传统开发中,如果A类依赖B类,会在A类中创建B类的实例,如果B类增加一个属性,那么使用B类的构造方法需要修改代码…

Java基于SpringBoot的原创歌曲分享平台

文章目录 1 简介2 技术栈3 需求分析4 平台设计主要功能5 平台实现5.1平台功能模块 5.2后台功能模块52.1管理员功能模块5.2.2用户功能模块 源码咨询 1 简介 原创歌曲分享平台&#xff0c;为了随时随地查看原创歌曲分享信息提供了便捷的方法&#xff0c;更重要的是大大的简化了管…

外卖霸王餐平台究竟是如何运作的?以及盈利点到底在哪里?

外卖霸王餐 1、业务简介。业务模式是消费者以5-10元吃到原价15-25元的外卖&#xff0c;底层逻辑是帮外卖商家做推广&#xff0c;解决新店基础销量、老店增加单量、品牌打万单店的需求。 因为外卖店的平均生命周期只有6个月&#xff0c;不断有新店愿意送霸王餐。部分老店也愿…

指南:通过 NFTScan API 获取钱包地址的 NFT Statistics 全量数据

获取钱包地址的全量 NFT 及统计分析数据对于开发者和投资者来说都是十分重要的。具体来说&#xff1a;对开发者而言&#xff0c;获取每个钱包的完整资产数据&#xff0c;并进行统计分析&#xff0c;是构建钱包管理工具、资产分析应用的基础&#xff0c;这些应用都需要全面且精确…

安装typescript之后提示不是内部命令

解决方案&#xff1a; 1、删除C:\Users\用户\下的.npmrc文件 2、在命令行输入npm cache clean --force 以上提示表示执行成功 3、重新安装typescript npm install -g typescript tsc

算法-堆/多路归并-查找和最小的 K 对数字

算法-堆/多路归并-查找和最小的 K 对数字 1 题目概述 1.1 题目出处 https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/description/?envTypestudy-plan-v2&envIdtop-interview-150 1.2 题目描述 2 优先级队列构建大顶堆 2.1 思路 将两个数字的和放入大…

同一台电脑下的wireshark的http抓包查看使用的接口

开发过程中写软件开发设计时需要写调用的接口&#xff0c; 可以使用抓包软件 操作一遍&#xff0c;看抓包记录 然后看自己需要的接口调用情况 同一台电脑用这个 设置需要的抓包协议 在后台搜索关键词也可以看到用了哪些接口 Json查看器也可以查看接口信息

Vue+element开发Simple Admin后端管理系统页面

最近看到各种admin&#xff0c;头大&#xff0c;内容太多&#xff0c;根本不知道怎么改。所以制作了这个项目&#xff0c;只包含框架、和开发中最常用的表格和表单&#xff0c;不用自己从头搭建架构&#xff0c;同时也容易上手二次开发。可以轻松从其他开源项目整合到本项目。项…

避障技术再提升,扫地机器人避障不止于精准

扫地机器人好用与否&#xff0c;避障表现首当其冲&#xff0c;那么评判避障好坏的标准又是什么&#xff1f; 有效避障仅是第一步 时至今日&#xff0c;可以说仍有相当一部分人对于扫地机器人的印象停留在人工“智障”上&#xff0c;由于早期的产品基本不具备避障能力&#xf…

ArcGIS 10.7软件安装包下载及安装教程!

【软件名称】&#xff1a;ArcGIS 10.7 【安装环境】&#xff1a;Windows 【下载链接 】&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1IwsPubYWGHd9ztmn45QLJA 提取码&#xff1a;1oeq 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 软件简介 ArcGIS…

ubuntu16编译linux源码内核

一、环境准备 1.1、安装虚拟机ubuntu16 编译内核大概需要20G的磁盘空间&#xff0c;所以硬盘大小尽量大于40G网络适配使用桥接 1.1.1、查看当前内核版本 uname -r1.2、安装samba服务 Samba 是一款数据共享的软件&#xff0c;可用于 Ubuntu 与 Windows 之间共享源代码&#…