📍 题目背景
各位家人们,今天咱们来拆解一道算法面试中极其经典的字符串难题:438. 找到字符串中所有字母异位词。 这道题可以说是"滑动窗口(Sliding Window)"的教科书级经典应用,不仅考察了对窗口边界的掌控力,还考察了如何极致压缩时间复杂度。
【题目描述】 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 字母异位词:指字母相同,但排列不同的字符串。
【示例】
输入: s =
"cbaebabacd", p ="abc"输出:[0,6]解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
✨ 核心破题逻辑:定长滑动窗口 + 频率数组
直觉陷阱 :每次从 s 里截取一段跟 p 一样长的字符串,排个序再对比? 致命后果:每一次排序都需要额外时间,加上外层循环,如果字符串超级长,绝对会超时(TLE)!
【降维打击:定长滑动窗口】 我们要找的是异位词,说白了就是"字母种类和数量一致,不管顺序"。 既然题目说明了"只包含小写字母",我们就可以用两个大小为 26 的数组(scount 和 pcount)来当做计数的"备忘录",这比真正的哈希表要快得多!
破题三步曲:
-
打造标尺 (
pcount) :先把目标字符串p里的字母频率统计出来,这是我们的"核对标准"。 -
初始化第一个窗口 :在
s的最左边,先吃进一个和p同样长度的窗口,统计出scount,并和pcount进行第一次核对。 -
窗口开始滑动(灵魂操作):
-
窗口整体向右平移一格。
-
吃进新字符 :右边新进来的字母,在
scount里对应数量+1。 -
吐出旧字符 :左边被移出窗口的字母,在
scount里对应数量-1。 -
每次滑动,只发生这两次简单的加减法操作,完全不需要重新扫描整个窗口!然后立刻比对
scount和pcount。
-
💻 C++ 满分 AC 代码
掌握了上述逻辑,我们来看看优雅的 C++ 实现。注意代码中的几个防坑细节!
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
// 🚀 防坑 1:兜底防线!如果 s 比 p 还短,直接下班,防止越界报错
if (s.size() < p.size()) {
return {};
}
// 🚀 亮点:用数组代替哈希表,速度起飞!
vector<int> scount(26, 0);
vector<int> pcount(26, 0);
vector<int> res;
// 1. 初始化第一个窗口和目标标尺
for(int i = 0; i < p.size(); i++) {
scount[s[i] - 'a']++;
pcount[p[i] - 'a']++;
}
// 第一次核对
if(scount == pcount) {
res.push_back(0);
}
// 2. 窗口开始向右滑动
for(int i = p.size(); i < s.size(); i++) {
// 吃进右边界的新字符
scount[s[i] - 'a']++;
// 吐出左边界外的旧字符 (原来左边界的字符下标是 i - p.size())
scount[s[i - p.size()] - 'a']--;
// 每次滑动后进行核对
if(scount == pcount) {
res.push_back(i - p.size() + 1);
}
}
return res;
}
};
🔍 硬核原理解析:大厂面试加分项
如果你能在面试中讲出以下两点,面试官绝对会对你刮目相看:
1. 神奇的 s[i] - 'a' 是什么意思?
这其实是利用了底层 ASCII 码的物理特性。 在计算机底层,小写字母 'a' 到 'z' 是按顺序排列的数字(97 到 122)。 当我们写出 s[i] - 'a' 时,就是用当前的字母减去 'a':
-
如果是
'a',97 - 97 = 0,正好放进 0 号抽屉。 -
如果是
'b',98 - 97 = 1,正好放进 1 号抽屉。 -
以此类推...... 这个极其巧妙的数学减法,让我们省去了构建复杂哈希表的巨大开销!
2. C++ vector 的重载运算符 ==
在 C++ 中,如果我们比较两个类型相同、大小相等的 vector,可以直接使用 scount == pcount。底层会自动帮我们逐个元素进行对比,这不仅让代码极其简洁,而且由于是定长(26个元素)的连续内存块,对比速度极快!
📊 复杂度分析
-
时间复杂度: O(N)。这里 N 是字符串
s的长度。虽然每次循环都要对比两个大小为 26 的数组,但 26 是常数级操作(相当于 O(26 \\times N)),最终时间复杂度依然是极速的 O(N)。全程没有冗余的重复扫描! -
空间复杂度: O(1)。我们仅仅申请了两个大小固定为 26 的整数数组,无论输入字符串有多长,消耗的内存空间都不会增加。
💡 总结: "定长滑动窗口"的核心就在于那句口诀:吃进来一个,吐出去一个 。只要把控好这个边界,再结合 ASCII码相减 映射到数组的高阶技巧,任何同类型的字符串匹配难题都会在你面前迎刃而解!
附上动画演示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滑动窗口魔法:代码同步演示</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.cell-transition { transition: all 0.3s ease-in-out; }
/* 字符方块状态 */
.char-box { border-color: #e5e7eb; background-color: #f9fafb; color: #9ca3af; }
.char-in-window { box-shadow: 0 0 10px rgba(59, 130, 246, 0.4); border-color: #3b82f6; background-color: #eff6ff; color: #1e3a8a; transform: scale(1.05); z-index: 5; }
.char-add { box-shadow: 0 0 15px rgba(34, 197, 94, 0.7); border-color: #22c55e; background-color: #dcfce7; color: #14532d; transform: scale(1.15); z-index: 10; }
.char-remove { box-shadow: 0 0 15px rgba(239, 68, 68, 0.7); border-color: #ef4444; background-color: #fee2e2; color: #7f1d1d; transform: scale(1.15); z-index: 10; opacity: 0.8; }
.char-match { box-shadow: 0 0 20px rgba(234, 179, 8, 0.8); border-color: #eab308; background-color: #fef9c3; color: #854d0e; transform: scale(1.1); z-index: 10; font-weight: 900; }
/* 频率表动画 */
.freq-match { background-color: #dcfce7 !important; color: #166534; font-weight: bold; }
.freq-mismatch { background-color: #fee2e2 !important; color: #991b1b; font-weight: bold; }
/* 代码高亮样式 */
.code-line { transition: all 0.2s; border-left: 4px solid transparent; }
.code-highlight { background-color: rgba(59, 130, 246, 0.25); border-left-color: #3b82f6; color: #ffffff !important; text-shadow: 0 0 5px rgba(255,255,255,0.3); }
.code-keyword { color: #c678dd; } /* 紫色 */
.code-type { color: #e5c07b; } /* 黄色 */
.code-var { color: #e06c75; } /* 红色 */
.code-num { color: #d19a66; } /* 橙色 */
.code-comment { color: #5c6370; font-style: italic; } /* 灰色 */
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-2 md:p-4 font-sans text-gray-800">
<div class="max-w-7xl w-full bg-white rounded-2xl shadow-2xl overflow-hidden flex flex-col">
<!-- 头部标题 -->
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white p-4 md:p-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold mb-2">🔍 滑动窗口 x 核心代码 同步演示</h1>
<p class="text-blue-200 text-sm md:text-base">透视底层逻辑:观察动画的同时,看看 C++ 代码走到哪一行了!</p>
</div>
<!-- 动画主界面 (分左右两栏) -->
<div class="p-4 md:p-6 flex-1 flex flex-col lg:flex-row gap-6">
<!-- 左侧:动画演示区 -->
<div class="flex-1 flex flex-col border-r-0 lg:border-r-2 border-gray-100 lg:pr-6">
<!-- 讲解信息框 -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r-xl mb-6 min-h-[90px] flex items-center shadow-sm">
<p id="message-box" class="text-base md:text-lg font-medium text-blue-900 leading-relaxed">
准备开始!用长度为 3 的"滑动窗口"扫描字符串,并实时比对频率表。
</p>
</div>
<!-- 数据面板:频率表 & 结果 -->
<div class="flex flex-col sm:flex-row gap-4 mb-8 items-stretch justify-center">
<div class="bg-gray-50 p-3 rounded-xl border-2 border-gray-100 shadow-inner flex-1 max-w-sm mx-auto w-full">
<h3 class="text-center font-bold text-gray-500 mb-2 text-xs tracking-widest uppercase">📊 字母频率表</h3>
<div class="grid grid-cols-6 gap-1 text-center">
<div class="font-bold text-gray-400 text-xs py-1 border-b">字母</div>
<div class="font-bold text-gray-400 text-xs py-1 border-b">a</div>
<div class="font-bold text-gray-400 text-xs py-1 border-b">b</div>
<div class="font-bold text-gray-400 text-xs py-1 border-b">c</div>
<div class="font-bold text-gray-400 text-xs py-1 border-b">d</div>
<div class="font-bold text-gray-400 text-xs py-1 border-b">e</div>
<div class="font-bold text-blue-600 text-xs py-1.5 bg-blue-50 rounded-l">pCount</div>
<div class="font-mono text-gray-700 py-1.5 bg-blue-50">1</div>
<div class="font-mono text-gray-700 py-1.5 bg-blue-50">1</div>
<div class="font-mono text-gray-700 py-1.5 bg-blue-50">1</div>
<div class="font-mono text-gray-400 py-1.5 bg-blue-50">0</div>
<div class="font-mono text-gray-400 py-1.5 bg-blue-50 rounded-r">0</div>
<div class="font-bold text-indigo-600 text-xs py-1.5 bg-indigo-50 rounded-l mt-1">sCount</div>
<div id="count-a" class="font-mono text-gray-700 py-1.5 bg-indigo-50 mt-1 cell-transition">0</div>
<div id="count-b" class="font-mono text-gray-700 py-1.5 bg-indigo-50 mt-1 cell-transition">0</div>
<div id="count-c" class="font-mono text-gray-700 py-1.5 bg-indigo-50 mt-1 cell-transition">0</div>
<div id="count-d" class="font-mono text-gray-400 py-1.5 bg-indigo-50 mt-1 cell-transition">0</div>
<div id="count-e" class="font-mono text-gray-400 py-1.5 bg-indigo-50 mt-1 cell-transition rounded-r">0</div>
</div>
</div>
<div class="bg-white p-3 rounded-xl shadow border border-green-200 flex flex-col items-center justify-center min-w-[120px]">
<div class="text-xs text-green-600 font-bold mb-1">✅ 结果起始下标</div>
<div id="results-box" class="text-2xl font-black text-green-500">[]</div>
</div>
</div>
<!-- 字符串 S 展示区 -->
<div class="mb-8 relative">
<h3 class="text-xs font-bold text-gray-400 mb-2 ml-2">主字符串 s</h3>
<div class="flex flex-nowrap justify-start md:justify-center gap-1 md:gap-2 overflow-x-auto pb-2 px-1" id="string-container">
<!-- 方块动态生成 -->
</div>
</div>
<!-- 控制按钮区 -->
<div class="flex justify-center gap-3 mt-auto flex-wrap">
<button id="btn-reset" class="px-5 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold rounded-full shadow transition-colors text-sm">
🔄 重置
</button>
<button id="btn-step" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-bold rounded-full shadow transition-transform transform hover:scale-105 text-sm">
👣 下一步
</button>
<button id="btn-play" class="px-6 py-2 bg-emerald-500 hover:bg-emerald-600 text-white font-bold rounded-full shadow transition-transform transform hover:scale-105 text-sm">
▶️ 自动播放
</button>
</div>
</div>
<!-- 右侧:代码同步高亮区 -->
<div class="w-full lg:w-5/12 bg-[#282c34] rounded-xl shadow-inner overflow-hidden flex flex-col">
<div class="bg-[#21252b] px-4 py-2 flex items-center border-b border-gray-700">
<div class="flex gap-1.5 mr-4">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<span class="text-gray-400 text-xs font-mono">Solution.cpp</span>
</div>
<div class="p-4 font-mono text-xs md:text-sm text-gray-300 overflow-y-auto leading-loose flex-1">
<div id="line-1" class="code-line px-2 py-0.5 code-comment">// 1. 初始化频率数组</div>
<div id="line-2" class="code-line px-2 py-0.5"><span class="code-type">vector</span><<span class="code-type">int</span>> pCount(26, 0), sCount(26, 0);</div>
<div id="line-3" class="code-line px-2 py-0.5 code-comment mt-2">// 2. 建立初始窗口</div>
<div id="line-4" class="code-line px-2 py-0.5"><span class="code-keyword">for</span> (<span class="code-type">int</span> i = 0; i < pLen; i++) {</div>
<div id="line-5" class="code-line px-2 py-0.5 pl-6">pCount[p[i] - 'a']++;</div>
<div id="line-6" class="code-line px-2 py-0.5 pl-6">sCount[s[i] - 'a']++;</div>
<div id="line-7" class="code-line px-2 py-0.5">}</div>
<div id="line-8" class="code-line px-2 py-0.5 code-comment mt-2">// 3. 核对第一个窗口</div>
<div id="line-9" class="code-line px-2 py-0.5"><span class="code-keyword">if</span> (sCount == pCount) {</div>
<div id="line-10" class="code-line px-2 py-0.5 pl-6">res.push_back(0);</div>
<div id="line-11" class="code-line px-2 py-0.5">}</div>
<div id="line-12" class="code-line px-2 py-0.5 code-comment mt-2">// 4. 窗口开始向右滑动</div>
<div id="line-13" class="code-line px-2 py-0.5"><span class="code-keyword">for</span> (<span class="code-type">int</span> i = pLen; i < sLen; i++) {</div>
<div id="line-14" class="code-line px-2 py-0.5 pl-6">sCount[s[i] - 'a']++; <span class="code-comment ml-2">// 吃进新字符</span></div>
<div id="line-15" class="code-line px-2 py-0.5 pl-6">sCount[s[i - pLen] - 'a']--; <span class="code-comment ml-2">// 吐出旧字符</span></div>
<div id="line-16" class="code-line px-2 py-0.5 pl-6 mt-1"><span class="code-keyword">if</span> (sCount == pCount) {</div>
<div id="line-17" class="code-line px-2 py-0.5 pl-10 text-[#56b6c2]">res.push_back(i - pLen + 1);</div>
<div id="line-18" class="code-line px-2 py-0.5 pl-6">}</div>
<div id="line-19" class="code-line px-2 py-0.5">}</div>
</div>
</div>
</div>
</div>
<script>
const s = "cbaebabacd";
const p = "abc";
const pLen = p.length;
const sLen = s.length;
let currentFrameIndex = 0;
let isPlaying = false;
let playInterval;
let frames = [];
const targetCounts = { a: 1, b: 1, c: 1, d: 0, e: 0 };
function checkMatch(counts) {
for (let char of ['a', 'b', 'c', 'd', 'e']) {
if ((counts[char] || 0) !== targetCounts[char]) return false;
}
return true;
}
function generateFrames() {
frames = [];
let currentCounts = { a: 0, b: 0, c: 0, d: 0, e: 0 };
let results = [];
frames.push({
sCount: {...currentCounts}, window: [-1, -1], results: [...results],
msg: `首先初始化频率数组 <b>pCount</b> 和 <b>sCount</b>,大小都是 26,初始为 0。`,
activeCode: [1, 2] // 对应 HTML 中的 line id
});
// 1. 初始化第一个窗口
frames.push({
sCount: {...currentCounts}, window: [0, 0], actionIdx: 0, actionType: 'add', results: [...results],
msg: `【初始窗口】将左边前 ${pLen} 个字符吃进窗口。吃进 '${s[0]}'。`,
activeCode: [3, 4, 5, 6, 7]
});
currentCounts[s[0]]++;
frames.push({
sCount: {...currentCounts}, window: [0, 1], actionIdx: 1, actionType: 'add', results: [...results],
msg: `【初始窗口】吃进 '${s[1]}'。`,
activeCode: [3, 4, 5, 6, 7]
});
currentCounts[s[1]]++;
frames.push({
sCount: {...currentCounts}, window: [0, 2], actionIdx: 2, actionType: 'add', results: [...results],
msg: `【初始窗口】吃进 '${s[2]}'。初始窗口建立完毕,准备核对。`,
activeCode: [3, 4, 5, 6, 7]
});
currentCounts[s[2]]++;
let isMatch = checkMatch(currentCounts);
if (isMatch) results.push(0);
frames.push({
sCount: {...currentCounts}, window: [0, 2], match: isMatch, results: [...results],
msg: `检查 sCount 和 pCount 是否相等。${isMatch ? '🎉 匹配成功!记录下标 0。' : '不匹配。'}`,
activeCode: isMatch ? [8, 9, 10, 11] : [8, 9, 11]
});
// 2. 窗口滑动
for (let i = pLen; i < sLen; i++) {
let leftOut = i - pLen;
frames.push({
sCount: {...currentCounts}, window: [leftOut, i], actionIdx: i, actionType: 'add', results: [...results],
msg: `【滑动】指针向右,新吃进 <b>'${s[i]}'</b>。频率 +1。`,
activeCode: [12, 13, 14]
});
currentCounts[s[i]]++;
frames.push({
sCount: {...currentCounts}, window: [leftOut + 1, i], actionIdx: leftOut, actionType: 'remove', results: [...results],
msg: `【滑动】为了保持长度为 ${pLen},必须吐出最左侧旧字符 <b>'${s[leftOut]}'</b>。频率 -1。`,
activeCode: [15]
});
currentCounts[s[leftOut]]--;
isMatch = checkMatch(currentCounts);
if (isMatch) results.push(leftOut + 1);
frames.push({
sCount: {...currentCounts}, window: [leftOut + 1, i], match: isMatch, results: [...results],
msg: `比对更新后的窗口。${isMatch ? `🎉 匹配成功!记录起始下标 <b>${leftOut + 1}</b>。` : '不匹配。'}`,
activeCode: isMatch ? [16, 17, 18] : [16, 18]
});
}
frames.push({
sCount: {...currentCounts}, window: [-1, -1], results: [...results], end: true,
msg: `🏁 循环结束,返回结果。核心操作全在循环内的常数级加减,完美 O(N)!`,
activeCode: []
});
}
function render() {
const frame = frames[currentFrameIndex];
// 1. UI 文本
document.getElementById('message-box').innerHTML = frame.msg;
document.getElementById('results-box').innerHTML = '[' + frame.results.join(', ') + ']';
// 2. 代码高亮同步
for (let i = 1; i <= 19; i++) {
const lineEl = document.getElementById(`line-${i}`);
if (!lineEl) continue;
if (frame.activeCode && frame.activeCode.includes(i)) {
lineEl.classList.add('code-highlight');
} else {
lineEl.classList.remove('code-highlight');
}
}
// 3. 频率表
for (let char of ['a', 'b', 'c', 'd', 'e']) {
const el = document.getElementById(`count-${char}`);
const currentVal = frame.sCount[char] || 0;
el.innerText = currentVal;
el.classList.remove('freq-match', 'freq-mismatch', 'text-gray-400');
if (frame.match !== undefined) {
if (currentVal === targetCounts[char]) el.classList.add('freq-match');
else el.classList.add('freq-mismatch');
} else {
if (currentVal === 0 && targetCounts[char] === 0) el.classList.add('text-gray-400');
else el.classList.add('text-gray-800');
}
}
// 4. 字符串
const container = document.getElementById('string-container');
container.innerHTML = '';
const [winStart, winEnd] = frame.window;
for (let i = 0; i < sLen; i++) {
const wrapper = document.createElement('div');
wrapper.className = 'flex flex-col items-center flex-shrink-0';
const idxLabel = document.createElement('div');
idxLabel.className = 'text-[10px] md:text-xs font-bold text-gray-400 mb-1';
idxLabel.innerText = i;
const box = document.createElement('div');
let boxClasses = 'w-9 h-9 md:w-12 md:h-12 flex items-center justify-center text-lg md:text-xl font-bold border-[3px] rounded-lg cell-transition ';
if (frame.end) boxClasses += 'char-box opacity-50';
else if (frame.match && i >= winStart && i <= winEnd) boxClasses += 'char-match';
else if (i === frame.actionIdx) {
if (frame.actionType === 'add') boxClasses += 'char-add';
if (frame.actionType === 'remove') boxClasses += 'char-remove';
} else if (i >= winStart && i <= winEnd) boxClasses += 'char-in-window';
else boxClasses += 'char-box';
box.className = boxClasses;
box.innerText = s[i];
wrapper.appendChild(idxLabel);
wrapper.appendChild(box);
container.appendChild(wrapper);
}
const isEnd = currentFrameIndex >= frames.length - 1;
document.getElementById('btn-step').disabled = isEnd;
document.getElementById('btn-step').className = isEnd
? "px-6 py-2 bg-gray-300 text-gray-500 font-bold rounded-full cursor-not-allowed shadow text-sm"
: "px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-bold rounded-full shadow transition-transform transform hover:scale-105 text-sm";
}
// --- 控制逻辑 ---
document.getElementById('btn-step').addEventListener('click', () => {
if (currentFrameIndex < frames.length - 1) { currentFrameIndex++; render(); }
});
document.getElementById('btn-reset').addEventListener('click', () => {
currentFrameIndex = 0; if(isPlaying) togglePlay(); render();
});
document.getElementById('btn-play').addEventListener('click', togglePlay);
function togglePlay() {
const playBtn = document.getElementById('btn-play');
if (isPlaying) {
clearInterval(playInterval);
playBtn.innerHTML = '▶️ 自动播放';
playBtn.classList.replace('bg-orange-500', 'bg-emerald-500');
playBtn.classList.replace('hover:bg-orange-600', 'hover:bg-emerald-600');
isPlaying = false;
} else {
if (currentFrameIndex >= frames.length - 1) currentFrameIndex = 0;
playBtn.innerHTML = '⏸️ 暂停播放';
playBtn.classList.replace('bg-emerald-500', 'bg-orange-500');
playBtn.classList.replace('hover:bg-emerald-600', 'hover:bg-orange-600');
isPlaying = true;
playInterval = setInterval(() => {
if (currentFrameIndex < frames.length - 1) {
currentFrameIndex++; render();
} else { togglePlay(); }
}, 1500); // 稍微放慢到1.5秒一帧,方便看清代码
}
}
generateFrames();
render();
</script>
</body>
</html>