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

news/2024/5/20 9:21:00 标签: 滑动窗口, 字符串

文章目录

  • 写在前面
  • 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.niftyadmin.cn/n/5056942.html

相关文章

07贪心:跳跃游戏II

07贪心&#xff1a;跳跃游戏II 45. 跳跃游戏 II 本题相对于55.跳跃游戏还是难了不少。 但思路是相似的&#xff0c;还是要看最大覆盖范围。 本题要计算最少步数&#xff0c;那么就要想清楚什么时候步数才一定要加一呢&#xff1f; 贪心的思路&#xff0c;局部最优&#xf…

安卓机型不需要解锁bl 不需要root 即可安装模块 框架 VirtualXposed使用步骤分析

​​​​​​安卓玩机教程---全机型安卓4----安卓12 框架xp edx lsp安装方法【一】 安卓系列机型 框架LSP 安装步骤 支持多机型 LSP框架通用安装步骤 通过以上两个博文基本可以了解手机正常安装框架的步骤。但很多机型局限于不能解锁bl和root&#xff0c;那么这些机型能不能使…

Zilliz@阿里云:大模型时代下Milvus Cloud向量数据库处理非结构化数据的最佳实践

大模型时代下的数据存储与分析该如何处理?有没有已经落地的应用实践? 为探讨这些问题,近日,阿里云联合 Zilliz 和 Doris 举办了一场以《大模型时代下的数据存储与分析》为主题的技术沙龙,其中,阿里云对象存储 OSS 上拥有海量的非结构化数据,Milvus(Zilliz)作为全球最有…

了解vtk显示的原理

文章目录 目标:知识补充:1.什么是图元?2.最让我不解的是:官方讲的是:mapper讲polydata转换为可渲染的图元数据,然后actor是将polydata映射为可渲染的图元???既然mapper就已经将其解析为图元数据,为什么actor还要进一步解析呢?3.那polydata不是也获得了一些数据,这些数据是…

蓝桥等考Python组别五级007

第一部分:选择题 1、Python L5 (15分) 表达式“not a > 0”等价于下面哪个表达式?( ) a < 0a == 0a <= 0a in 0正确答案:C 2、Python L5 (15分) 执行下面的程序,当用键盘输入10时,输出结果是( )。 n &

STM32三种开发方式及标准库和HAL库的编程差异

三种开发方式 STM32基于标准库函数和HAL库编程差异_stm32库函数和hal库-CSDN博客本文目的是以串口通信来简要分析STM32使用标准库函数和HAL库函数编程的差异。目录&#xff08;一&#xff09;开发方式1.配置寄存器2.库函数3.HAL库&#xff08;二&#xff09;库函数与HAL库对比…

三个要点,掌握Spring Boot单元测试

单元测试是软件开发中不可或缺的重要环节&#xff0c;它用于验证软件中最小可测试单元的准确性。结合运用Spring Boot、JUnit、Mockito和分层架构&#xff0c;开发人员可以更便捷地编写可靠、可测试且高质量的单元测试代码&#xff0c;确保软件的正确性和质量。 一、介绍 本文…

0x23根据地址读取内存服务

诊断仪从ECU通过起始地址和内存大小空间来读取指定内存的数据&#xff0c;高地址范围未使用到的字节用0x00去填充。 请求报文格式 肯定响应报文形式