滑动窗口精讲:O (n) 解决无重复元素的最长子数组问题

0 阅读6分钟

在算法学习中,“无重复元素的最长子数组”是蓝桥杯等竞赛的高频考点。这道题看似简单,实则藏着从暴力枚举到线性时间算法的优化精髓。本文将结合手写笔记的核心思路,从暴力解法的痛点出发,拆解滑动窗口+哈希表的最优实现,带你彻底掌握这一经典通性通法。

一、问题重述 给定一个整数数组,找出其中所有元素互不重复的最长连续子数组的长度。

示例:输入 [1,2,3,4,5,3,6,7,8],输出 5(最长无重复子数组为 [1,2,3,4,5])。

二、从暴力到优化:思路演进 在动手写代码前,我们先梳理思考过程,理解“为什么要用滑动窗口”。

1. 暴力解法:两层循环的局限 最直观的思路是枚举所有子数组,再判断每个子数组是否有重复元素: - 外层循环:枚举子数组的起始位置 i; - 内层循环:枚举子数组的结束位置 j,遍历 [i,j] 统计元素出现次数; - 时间复杂度:枚举子数组是 O(n²),判断重复若用暴力遍历则再加 O(n),总复杂度 O(n³);即使优化判断重复的逻辑,最低也为 O(n²)。 对于 n=1e6 的数据规模(如题目中 const int N = 1e6 + 10),O(n²) 的算法会直接超时。这就需要我们利用单调性,找到更高效的方法。

2. 核心洞察:滑动窗口的单调性 手写笔记中提到关键性质:

在暴力枚举的过程中,left 以及 right 其实是可以不回退的。 我们可以用两个指针 leftright 维护一个动态窗口 [left, right],代表当前的“无重复元素子数组”。核心逻辑如下: 1. 右指针扩张right 逐个向右移动,将元素纳入窗口,探索更长的子数组; 2. 左指针收缩:当 right 指向的元素在窗口内重复时,left 必须向右移动(而非回退到起点),直到窗口内无重复; 3. 结果更新:每轮扩张后,计算当前窗口长度,保留最大值。 这种“右指针只前进、左指针不回退”的特性,让每个元素最多被访问两次(一次被 right 遍历,一次被 left 移出),时间复杂度直接降至 O(n)

三、核心工具:哈希表的作用 要实现滑动窗口,关键是快速判断当前元素是否在窗口内重复,这就需要用到哈希表(手写笔记中“借助哈希表 → O(1)”)。

1. 为什么选哈希表? 有同学会问:“用普通数组统计次数不行吗?” - 普通数组:仅适用于元素取值范围明确且较小的场景(如 0 ≤ a[i] ≤ 1e5)。若元素包含负数、超大数(如 1e9),数组下标无法覆盖,或会超出内存限制; - 哈希表(unordered_map):是通用方案,以「元素值 → 出现次数」的键值对形式存储,无论元素取值如何,都能在 O(1) 时间内完成“查询次数”和“更新次数”操作。 本文代码采用哈希表,保证对所有输入场景的兼容性。

四、完整代码实现(附逐行精讲) 结合手写笔记的“初始化-进口-判断-出口-更新结果”流程,以下是可直接用于竞赛的完整代码,并对核心逻辑逐行解析。

#include <bits/stdc++.h>
using namespace std; 
const int N = 1e6 + 10; // 适配1e6规模的输入 
int n; int a[N]; // 存储数组,下标从1开始(符合竞赛习惯) 
int main() { ios::sync_with_stdio(false); // 关闭同步流,加速
cin/cout cin.tie(nullptr); 
int T; cin >> T; // 多组测试用例 
while (T--) { cin >> n; 
for (int i = 1; i <= n; i++) 
{ cin >> a[i]; // 读取数组元素 } // ① 初始化:双指针+哈希表+结果变量 int left = 1, right = 1, ret = 0; unordered_map<int, int> mp; // 维护窗口内元素的出现次数 // ② 滑动窗口主循环:右指针遍历整个数组 
while (right <= n) { // 进口:让right指向的元素进入窗口,更新出现次数 mp[a[right]]++; // ③ 判断+出口:若当前元素重复,收缩左指针直到窗口合法 
while (mp[a[right]] > 1) 
{ mp[a[left]]--; // 左指针元素移出窗口,次数减1 left++; // 左指针右移 } // ④ 更新结果:取当前窗口长度的最大值 ret = max(ret, right - left + 1); // 右指针继续扩张 right++; } 
cout << ret << endl; // 输出每组用例的结果 } return 0; }  

关键步骤拆解

  1. 初始化left=1, right=1 表示窗口初始只有一个位置;mp 为空,ret=0 记录最大长度
  2. 进口mp[a[right]]++,将右指针元素加入哈希表,统计次数(不存在的键默认值为0,++后变为1)
  3. 判断合法性while (mp[a[right]] > 1),若当前元素出现次数大于1,说明窗口内有重复;
  4. 出口mp[a[left]]--; left++,将左指针元素移出窗口,直到重复元素被排除;
  5. 更新结果right - left + 1 是当前窗口的长度,用 max 函数保留最大值。

五、核心疑问解答

1. 为什么滑动窗口算法是正确的? 假设存在一个更长的无重复子数组 [L, R],那么在 right 遍历到 R 时,left 一定会收缩到 L。因为滑动窗口的左指针只会向右移动,不会错过任何可能的最优起始位置,最终必然能捕获到最长的无重复子数组。

2. 时间复杂度分析 - 右指针 right1 遍历到 n,共 n 次操作; - 左指针 left 最多从 1 移动到 n,共 n 次操作; - 哈希表的增删查操作均为 O(1); - 总时间复杂度:O(n),完美适配 n=1e6 的数据规模。 ## 六、拓展:何时用数组替代哈希表? 如果题目明确限定元素取值范围(如蓝桥杯常考的 0 ≤ a[i] ≤ 1e5),用数组替代哈希表会更高效(无哈希冲突开销)。 只需将代码中的哈希表替换为计数数组,并在每组测试用例前重置数组即可。

七、总结 “无重复元素的最长子数组”是滑动窗口算法的经典应用,其核心在于利用“双指针单调性”将暴力枚举的 O(n²) 优化为 O(n)。 本文的代码和思路是竞赛中的通性通法:不仅适用于本题,还可迁移到“最小覆盖子串”“最长重复子数组”等同类题目。掌握“哈希表维护窗口状态+双指针动态调整”的逻辑,就能轻松应对一大类子数组/子串问题。