绝对最通俗、最易懂、最详尽、最完美的讲解:滑动窗口为啥只“减左边、加右边”?中间的字符根本不用管!

🌈 绝对最通俗、最易懂、最详尽、最完美的讲解:滑动窗口为啥只“减左边、加右边”?中间的字符根本不用管!

你是不是一直想不通: “窗口一动,整个子串都变了,中间那么多字符,难道不需要重新数一遍吗?”

❌ 完全不需要!

✅ 只需两步:

扔掉最左边那个滑出去的字符

加上最右边那个新进来的字符

中间的字符?它们还在窗口里呢,一个都没少,当然不用动!

听起来还是迷糊?别急!这篇文章会用 幼儿园小朋友都能听懂的方式,从零开始,一步一步带你彻底搞明白!

🍭 一、故事时间:糖果店的小侦探

想象你是一个小侦探,任务是找出传送带上哪些连续的 3 颗糖果,和老板给你的“标准礼包”一模一样。

老板的标准礼包长这样:

标准礼包:[草莓] [香蕉] [苹果]

传送带上的糖果排成一排:

传送带: [橙子] [草莓] [香蕉] [苹果] [葡萄] [芒果]

你手里有一个能框住 3 颗糖果 的魔法框(这就是“窗口”)。

🔹 第一步:检查第一组糖果

你把魔法框放在最前面:

传送带: [橙子] [草莓] [香蕉] [苹果] [葡萄] [芒果]

┌───────────────┐

│ 橙子 草莓 香蕉 │ ← 当前窗口

└───────────────┘

↑ ↑ ↑

0 1 2

你拿出小本本,记录这三颗糖果的数量:

糖果数量橙子1草莓1香蕉1苹果0葡萄0芒果0

❌ 和标准礼包不一样(标准有苹果,没有橙子),所以不匹配。

🔹 第二步:魔法框向右滑一格

现在你要检查下一组连续的 3 颗糖果。 你把魔法框向右移动一格:

传送带: [橙子] [草莓] [香蕉] [苹果] [葡萄] [芒果]

┌───────────────┐

│ 草莓 香蕉 苹果 │ ← 新窗口

└───────────────┘

↑ ↑ ↑

1 2 3

现在窗口里的糖果是:[草莓][香蕉][苹果]

问题来了: 你需要把这三颗糖果全部拆开,重新一颗一颗数吗?

❌ 不需要!

因为你上一轮已经知道“草莓”和“香蕉”的数量了!

你只需要做两件事:

扔掉“橙子” → 因为它滑出窗口了,不再属于当前组。

加上“苹果” → 因为它刚滑进来,现在属于当前组。

中间的“草莓”和“香蕉”呢? 它们还在窗口里啊!位置变了,但它们本身没变,数量当然也不用改!

所以更新后的小本本:

橙子:1 → 0 (扔掉了)

苹果:0 → 1 (新加的)

草莓:还是 1

香蕉:还是 1

现在看看:

草莓:1, 香蕉:1, 苹果:1 → ✅ 和标准礼包一模一样!

所以这一组匹配!起始位置是 1。

🔹 第三步:再滑一格

继续向右移动魔法框:

传送带: [橙子] [草莓] [香蕉] [苹果] [葡萄] [芒果]

┌───────────────┐

│ 香蕉 苹果 葡萄 │ ← 新窗口

└───────────────┘

↑ ↑ ↑

2 3 4

现在要检查 [香蕉][苹果][葡萄]

怎么做?

扔掉最左边那个滑出去的:s[1] = '草莓' → 草莓: 1 → 0

加上最右边那个新进来的:s[4] = '葡萄' → 葡萄: 0 → 1

中间的“香蕉”和“苹果”:它们还在窗口里,数量不变!

更新后:

香蕉:1, 苹果:1, 葡萄:1 → ❌ 和标准礼包不同(多了葡萄,少了草莓),不匹配。

🧩 二、关键真相:中间字符根本没“消失”!

你发现了吗?

每次窗口滑动,只有两个角色发生变化:

退出者:最左边的那个字符,它被“踢出”了窗口。

新成员:最右边的新字符,它“加入”了窗口。

中间的所有字符,它们只是在窗口里往左挪了一个位置,但仍然在窗口内,所以它们的数量完全不需要改变!

这就像是一个排队的队伍:

旧队伍:[A][B][C][D] ← 窗口大小=4

向右滑动后:

新队伍: [B][C][D][E]

A 走了(减掉)

E 加入了(加上)

B、C、D 呢?它们只是往前挪了一步,人还在队里!

📊 三、回到代码:频次数组就是你的“小本本”

我们用一个长度为 26 的数组(比如 count[26])来记录当前窗口里每个字母的数量。

这个数组就像你的“小本本”,记着当前窗口里:

'a' 有几个

'b' 有几个

……

'z' 有几个

滑动窗口时,你不是换一本新本子,而是在原来的本子上修改两行!

// 窗口向右滑动

--count[s[i] - 'a']; // 旧本子上划掉最左边字符的数量(它走了)

++count[s[i + k] - 'a']; // 旧本子上给新字符加1(它来了)

// 其他所有字符的数量都不动!

然后你拿这个更新后的“小本本”去和“标准本子”(p 的频次)对比,看是否一样。

⏱ 四、效率大比拼:暴力法 vs 滑动窗口

假设你要检查 1000 个长度为 100 的子串。

方法每次操作总操作次数暴力法每次都重新数 100 个字符1000 × 100 = 100,000 次滑动窗口每次只改 2 个字符1000 × 2 = 2,000 次

👉 快了 50 倍!

这就是“聪明工作” vs “蛮力工作”的区别!

✅ 五、完整正确代码(超级注释版)

#include

#include

using namespace std;

vector findAnagrams(string s, string p) {

int n = s.size(); // 主串长度

int k = p.size(); // 目标串长度(窗口大小)

// 如果主串比目标串短,肯定找不到

if (n < k) {

return {};

}

vector ans; // 存答案:所有匹配的起始位置

vector windowCount(26, 0); // 当前窗口的字符频次(小本本)

vector targetCount(26, 0); // 目标 p 的字符频次(标准本子)

// 第一步:初始化——统计 p 的频次 + s 的第一个窗口

for (int i = 0; i < k; ++i) {

windowCount[s[i] - 'a']++; // 第一个窗口:s[0] 到 s[k-1]

targetCount[p[i] - 'a']++; // p 的所有字符频次

}

// 检查第一个窗口是否匹配

if (windowCount == targetCount) {

ans.push_back(0); // 起始位置是 0

}

// 第二步:滑动窗口!从 i=0 开始,到 i = n-k-1 结束

for (int i = 0; i < n - k; ++i) {

// 🟡 左边字符 s[i] 要滑出去了 → 减掉

windowCount[s[i] - 'a']--;

// 🟢 右边新字符 s[i + k] 要滑进来了 → 加上

windowCount[s[i + k] - 'a']++;

// 💡 此时 windowCount 已经是新窗口的频次了!

// 新窗口的起始位置是 i+1

if (windowCount == targetCount) {

ans.push_back(i + 1);

}

}

return ans;

}

❓ 六、你可能还有的疑问

Q1:如果字符串很长,中间字符会不会“累积误差”?

A:不会!因为我们只是做简单的加减,没有浮点数,不会出错。

Q2:为什么数组比较 == 是安全的?

A:C++ 的 vector 支持直接比较,会逐个元素对比,只要 26 个数都相等就返回 true。

Q3:能不能用 map?

A:可以,但太慢了!26 个字母用数组是最快的。

🎯 七、一句话总结(背下来!)

“窗口滑动如搬家,只搬头尾两件物;中间家具全留下,何须重新清点屋?”

🏁 八、结语:你已经超越了90%的人!

现在你不仅会写代码,更真正理解了背后的逻辑。

这种“利用历史信息避免重复计算”的思想,叫做 动态规划思维 或 滑动窗口优化,是算法中的高级技巧。

你已经掌握了它!

🎉 恭喜你!这篇博客的目标就是让你:

✅ 通俗:用糖果故事讲清楚

✅ 易懂:每一步都可视化

✅ 详尽:覆盖所有细节和疑问

✅ 完美:代码正确 + 思想透彻

如果你觉得这确实是“史上最完美讲解”,请把它收藏起来,以后教别人时就用这个比喻!

💬 有任何想法?欢迎留言讨论!我们一起把知识变得更简单!

📅 2025年9月12日 · 深夜 · 为你写下这篇温暖的算法课 🎯 愿每一个努力的人都能看懂每一个算法

Copyright © 2088 14年世界杯决赛_世界杯预选赛中国队出线形势 - pengxiaojing.com All Rights Reserved.
友情链接