当前位置: 首页 > 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.dtcms.com/a/340863.html

相关文章:

  • 《数据之舞》
  • GitHub宕机生存指南:从应急协作到高可用架构设计
  • QT-图像灰度处理时QImage.setPixel方法存在的坑
  • 在QT中动态生成控件造成界面卡顿时的鼠标处理
  • Qt设置软件使用期限【新版防修改系统时间】
  • 一个 WPF 文档和工具窗口布局容器
  • GitHub宕机应急指南:无缝协作方案
  • Eclipse 里Mybatis的xml的头部报错
  • 软考高级--系统架构设计师--案例分析真题解析
  • Java项目基本流程(五)
  • DeepSeek API 申请与 Node.js 对接指南
  • 服务器硬件电路设计之 SPI 问答(一):解密 SPI—— 从定义到核心特性
  • 服务器硬件电路设计之 SPI 问答(三):SPI 信号完整性守护与时钟频率的硬件设计羁绊
  • PCL+Spigot服务器+python进行MC编程2(使用RCON)---可以生成角色
  • 图论Day6学习心得
  • 源码编译部署 LAMP 架构详细步骤说明
  • 算法第五十二天:图论part03(第十一章)
  • 《算法导论》第 34 章 - NP 完全性
  • HTTP的协议
  • 【爬虫实战-IP代理的重要性二】 以Selenium为例
  • 在 Golang 中复用 HTTP 连接
  • JavaFx 动画-笔记
  • Docker操作速查表
  • MFQ测试分析与测试设计方法学习总结 (KYM)
  • 嵌入式开发学习———Linux环境下网络编程学习(四)
  • Java设计模式-命令模式
  • GitHub 热榜项目 - 日榜(2025-08-20)
  • Flask 之 Request 对象详解:全面掌握请求数据处理
  • 【NFTurbo】基于Redisson滑动窗口实现验证码发送限流
  • 如何在高并发下,保证共享数据的一致性