当前位置: 首页 > news >正文

C++ 力扣 438.找到字符串中所有字母异位词 题解 优选算法 滑动窗口 每日一题

文章目录

  • 一、题目描述
  • 二、为什么这道题值得我们弄懂?
  • 三、题目拆解:抓住关键约束
  • 四、思路演进:从暴力到滑动窗口
  • 五、算法实现:固定窗口的滑动与频率更新
    • 实现代码
    • 代码细节拆解:
    • 时间复杂度与空间复杂度
  • 六、实现过程中的坑点总结
  • 七、下题预告

这是封面原图,还有AI生成的动图,嘿嘿:
在这里插入图片描述
在这里插入图片描述

一、题目描述

题目链接:找到字符串中所有字母异位词

题目描述:
给定两个字符串  和 ,找到  中所有  的 **字母异位词** 的子串,返回这些子串的起始索引。字母异位词指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”,它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”,它是 “abc” 的字母异位词。

示例 2:
输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”,它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”,它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”,它是 “ab” 的字母异位词。

提示:
1 <= s.length, p.length <= 3 * 10^4
s 和 p 仅包含小写字母

二、为什么这道题值得我们弄懂?

作为滑动窗口在字符串匹配场景的经典例题,LeetCode 第 438 题的价值体现在:

  • 帮你掌握“固定窗口长度”的滑动窗口用法——不同于“水果成篮”中动态调整窗口大小,本题窗口长度由 p 的长度固定,是滑动窗口的另一类典型应用;
  • 带你理解“字符频率匹配”的核心逻辑——字母异位词的本质是“字符种类和数量完全相同”,如何通过统计频率高效判断匹配;
  • 让你学会“窗口滑动时的频率更新技巧”——如何避免重复计算,仅通过增减边界字符的频率实现高效维护。

学会这道题,能为解决“字符串子串匹配”“字符频率相关”的问题打下基础,比如后续要讲的“串联所有单词的子串”,核心思路也与此题相通。

三、题目拆解:抓住关键约束

结合题目要求和示例,核心要素如下:

  1. 输入是两个字符串 s(待搜索的主串)和 p(目标子串),长度均可达 3×10⁴(需考虑效率)。
  2. 核心约束是“子串需为 p 的字母异位词”,即:
    • 子串长度必须与 p 相同(因字母数量相同);
    • 子串中每种字符的出现次数与 p 完全一致。
  3. 目标是找到 s 中所有满足条件的子串的起始索引,并返回这些索引的列表。

关键点提炼

  • 窗口长度固定:子串长度必等于 p.size(),因此滑动窗口的大小是固定的(p.size());
  • 匹配核心是频率:无需关注字符顺序,只需统计窗口内字符频率与 p 的字符频率是否完全一致;
  • 效率要求高:需避免暴力枚举(枚举所有长度为 p.size() 的子串,再逐个统计频率),时间复杂度需控制在 O(n) 级别(n 为 s 的长度);
  • 边界处理重要:窗口滑动时,需准确更新进入窗口和离开窗口的字符频率,避免统计错误。

四、思路演进:从暴力到滑动窗口

1. 暴力解法
最直观的思路是:枚举 s 中所有长度为 p.size() 的子串,对每个子串统计字符频率,再与 p 的字符频率对比,若一致则记录起始索引。

  • 枚举方式
    起始索引 i 从 0 到 s.size()-p.size(),对每个 i,取子串 s[i:i+p.size()],用数组或哈希表统计其字符频率,再与 p 的频率数组对比。

  • 为什么不可行
    时间复杂度高。假设 p 的长度为 m,s 的长度为 n,则需枚举 n-m+1 个子串,每个子串统计频率需 O(m) 时间,对比频率也需 O(26) 时间(因只有小写字母),总时间复杂度为 O((n-m+1)×m),当 m 接近 n 时(如 m=1.5×10⁴,n=3×10⁴),会产生约 2×10⁸ 次操作,容易超时。

结论:必须用滑动窗口优化,通过“固定窗口+动态更新频率”减少重复计算,将时间复杂度降至 O(n)。

2. 滑动窗口的思路:固定窗口+频率匹配
滑动窗口的核心是:维护一个长度为 p.size() 的固定窗口,在 s 中从左到右滑动,通过动态更新窗口内的字符频率,实时判断窗口是否与 p 的频率匹配。

步骤拆解:

  • 预处理 p 的频率:用一个数组 hash1 统计 p 中每个字符的出现次数(因只有 26 个小写字母,数组大小设为 26 即可)。
  • 初始化窗口:用另一个数组 hash2 统计 s 中初始窗口(前 p.size() 个字符)的频率,同时用一个变量 count 记录“窗口中满足‘频率≤p中对应字符频率’的字符数量”(用于快速判断匹配)。
  • 滑动窗口
    • 向右移动窗口时,右侧新进入窗口的字符(in):更新 hash2[in],若更新后 hash2[in] 仍≤hash1[in],则 count 加 1(说明该字符的频率仍在合理范围内);
    • 左侧离开窗口的字符(out):更新 hash2[out],若更新前 hash2[out]hash1[out],则 count 减 1(说明该字符的频率已超出合理范围);
  • 判断匹配:若 count 等于 p.size(),说明窗口内所有字符的频率均与 p 一致(即窗口是 p 的字母异位词),记录当前窗口的起始索引。

关键问题:为什么用 count 而不是直接对比两个频率数组?
直接对比 hash1hash2 数组(每次滑动后遍历 26 个元素)也能判断匹配,但时间复杂度会增加 O(26)×(n-m+1)。而 count 的作用是“实时记录有效字符数”:当 count 等于 p.size() 时,意味着窗口内恰好有 p.size() 个字符,且每个字符的频率都未超过 p 中的频率(因只有满足 hash2[ch]≤hash1[ch] 时才会累计 count),结合窗口长度与 p 相同,此时必然是字母异位词。用 count 可将判断环节的时间复杂度从 O(26) 降至 O(1)。

五、算法实现:固定窗口的滑动与频率更新

实现代码

class Solution {
public:vector<int> findAnagrams(string s, string p) {vector<int> ret;  // 存储结果:所有满足条件的子串起始索引int m = p.size(), n = s.size();// 若s长度小于p,直接返回空(不可能有符合条件的子串)if (n < m) return ret;// hash1统计p中每个字符的频率(下标0-25对应a-z)int hash1[26] = {0};for (auto ch : p) hash1[ch - 'a']++;// hash2统计当前窗口中每个字符的频率int hash2[26] = {0};// left:窗口左边界;right:窗口右边界;count:窗口中"频率≤hash1对应频率"的字符总数for (int left = 0, right = 0, count = 0; right < n; right++) {// 右侧字符进入窗口char in = s[right];hash2[in - 'a']++;// 若进入后该字符频率仍≤p中的频率,说明是有效字符,count+1if (hash2[in - 'a'] <= hash1[in - 'a']) count++;// 当窗口长度超过p的长度时,左侧字符离开窗口if (right - left + 1 > m) {char out = s[left++];  // left右移前记录离开的字符// 若离开前该字符频率≤p中的频率,说明减少后有效字符数减少,count-1if (hash2[out - 'a'] <= hash1[out - 'a']) count--;hash2[out - 'a']--;  // 更新频率(注意先判断再减,否则判断条件错误)}// 当count等于p的长度时,窗口是p的字母异位词,记录起始索引leftif (count == m) ret.push_back(left);}return ret;}
};

代码细节拆解:

  1. 频率数组初始化
    hash1hash2 均为大小 26 的数组(对应 26 个小写字母),初始化为 0。hash1 先遍历 p 填充,记录 p 中每个字符的出现次数。

  2. 窗口初始化与扩展
    右指针 right 从 0 开始移动,每次将 s[right] 加入窗口(hash2[in-'a']++)。若加入后该字符的频率未超过 p 中的频率(hash2[in-'a']≤hash1[in-'a']),则 count 加 1——count 累计的是“窗口中符合频率要求的字符总数”。

  3. 窗口收缩(固定长度)
    当窗口长度(right-left+1)超过 p.size() 时,左指针 left 右移,同时将 s[left] 移出窗口。移出前需判断:若移出前该字符的频率≤p 中的频率,则 count 减 1(因该字符不再属于窗口,有效字符数减少),之后再更新 hash2[out-'a']--

  4. 匹配判断与结果记录
    count 等于 p.size() 时,说明窗口内所有字符的频率均与 p 一致(且窗口长度恰好为 p.size()),因此当前窗口是 p 的字母异位词,记录起始索引 left

时间复杂度与空间复杂度

时间复杂度
该算法的时间复杂度为 O(m + n),其中 m 是字符串 p 的长度,n 是字符串 s 的长度。
具体来说:

  • 预处理阶段,遍历 p 以统计字符频率,这一步的时间开销为 O(m);
  • 滑动窗口过程中,right 指针从 s 的起始位置移动到末尾,每个字符仅被访问一次,left 指针也仅单向移动,不会重复遍历,这部分的时间开销为 O(n);
  • 其余辅助操作(如更新频率数组、维护 count 变量等)均为常数级别的操作,时间开销可忽略。

当 m ≤ n 时,时间复杂度可简化为 O(n)。

空间复杂度
该算法的空间复杂度为 O(1)(常数级)。
具体来说:

  • 用于统计字符频率的两个数组 hash1hash2,大小均固定为 26(对应 26 个小写字母),与输入字符串 sp 的长度无关;
  • 其他变量(如 leftrightcount 等)均为单个整数,占用的空间为常数级;
  • 结果数组 ret 用于存储输出结果,属于必要的输出空间,通常不计入算法的额外空间复杂度。

因此,算法额外使用的空间为固定常数,空间复杂度为 O(1)。

六、实现过程中的坑点总结

  1. 窗口收缩时的顺序错误

    • 错误:先执行 hash2[out-'a']--,再判断是否需要减少 count
      例如:out 字符在 hash2 中的频率为 2,hash1 中为 2。若先减为 1,再判断“1≤2”,会错误地认为需要减 count,但实际移出前频率是 2(符合条件),应该减 count——但顺序颠倒后逻辑就错了。
    • 解决:先判断“移出前的频率是否≤hash1”,再更新 hash2 的频率。即先执行 if (hash2[out-'a'] <= hash1[out-'a']) count--;,再执行 hash2[out-'a']--;
  2. 忽略“s长度小于p”的边界情况

    • 错误:未判断 n < m 直接开始滑动,可能导致窗口长度始终无法达到 m,或数组访问越界。
    • 解决:开头加判断 if (n < m) return ret;,直接返回空结果。
  3. count的含义理解偏差

    • 错误:认为 count 是“窗口中与 p 共有的字符种类数”,而非“符合频率的字符总数”。
      例如:p 是 “aab”(hash1[a]=2, hash1[b]=1),窗口是 “aab”(hash2[a]=2, hash2[b]=1),此时 count 应等于 3(2个a+1个b,共3个字符均符合频率),而非 2(字符种类数)。
    • 解决:明确 count 统计的是“单个字符的数量”,而非“种类数”,只有当 count 等于 p.size() 时,才说明所有字符都匹配。
  4. 窗口长度判断错误

    • 错误:用 right - left > m 判断窗口是否超长(正确应为 right - left + 1 > m)。
      窗口长度是 right - left + 1(闭区间 [left, right] 的元素个数),例如 left=0,right=2 时,长度是 3,而非 2。
    • 解决:严格用 right - left + 1 > m 作为窗口超长的判断条件。

七、下题预告

明天将讲解 30. 串联所有单词的子串,这是滑动窗口在“多单词匹配”场景的进阶应用。

提前思考方向:

  • 题目中“串联所有单词的子串”的本质是什么?如何将其转化为可通过滑动窗口解决的问题?
  • 若单词长度固定,子串的长度是否固定?窗口大小如何设定?
  • 如何统计窗口内的单词频率,与目标单词列表的频率进行匹配?(与本题的字符频率匹配有何异同?)

如果觉得这篇解析有帮助,不妨:
🌟 点个赞,让更多人看到这份清晰思路
⭐ 收个藏,下次复习时能快速找回灵感
👀 加个关注,明天见!

http://www.lryc.cn/news/627035.html

相关文章:

  • Java 线程池ThreadPoolExecutor源码解读
  • 服务器内存条不识别及服务器内存位置图
  • linux的sysctl系统以及systemd系统。
  • 【网络运维】Linux 文本处理利器:sed 命令
  • MYSQL-增删查改CRUD
  • uni-app跨端开发最后一公里:详解应用上架各大应用商店全流程
  • 生产级的雪花算法
  • 自动驾驶导航信号使用方式调研
  • C语言实现全排列(非递归法)(以猪八戒买包子的故事为例解释)
  • SpringBoot 整合 Langchain4j RAG 技术深度使用解析
  • imx6ull-驱动开发篇30——Linux 非阻塞IO实验
  • redis---常用数据类型及内部编码
  • 设计具有功能安全和网络安全能力的新型半导体芯片
  • 攻克PostgreSQL专家认证
  • Unicode 字符串转 UTF-8 编码算法剖析
  • JVM面试精选 20 题(终)
  • SQL count(*)与 sum 区别
  • 第三阶段数据-4:SqlHelper类,数据库删除,DataTable创建
  • STM32F4 内存管理介绍及应用
  • 建模工具Sparx EA的多视图协作教程
  • PyTorch - Developer Notes
  • 吴恩达 Machine Learning(Class 3)
  • 国产化PDF处理控件Spire.PDF教程:如何使用 Python 添加水印到 PDF
  • Linux命令大全-ps命令
  • Linux系统之部署nullboard任务管理工具
  • 基于springboot中学信息技术课程教学网站
  • 栈上创建和堆上创建区别
  • Nginx 的完整配置文件结构、配置语法以及模块详解
  • 设计模式1-单例模式
  • 继续记事本项目