news 2026/7/27 14:51:55

7 月高频算法题回顾:滑动窗口、双指针与单调栈的精进

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
7 月高频算法题回顾:滑动窗口、双指针与单调栈的精进

7 月高频算法题回顾:滑动窗口、双指针与单调栈的精进

一、深度引言与场景痛点:刷了三遍还写不对的窗口

滑动窗口、双指针、单调栈——这三个技巧被称作"线型数据结构三板斧"。7 月的高频题,超过 40% 的题能用这三者之一优雅解决。但优雅的背后是无数次的边界崩溃。

最典型的场景:写一个滑动窗口求最长无重复子串,写得顺手,但遇到"最多包含 K 个不同字符的最长子串"时,同样的思路却怎么调都通不过。问题出在哪里?出在"窗口收缩条件"和"状态更新时机"这两个细节上。7 月我把这三个技巧的高频题重新做了一遍,每道题都画了状态转移图。这篇文章记录精进过程中的关键发现。

二、底层机制与原理深度剖析:三种技巧的本质统一

滑动窗口的本质是"维护一个满足条件的区间,在该区间上做增量计算"。这里有两个关键操作:窗口扩张时更新状态,窗口收缩时恢复状态。最容易出错的是"更新和收缩的先后顺序"。

以 LeetCode 3(无重复字符的最长子串)为例:遇到重复字符时,先收缩窗口还是先更新最大长度?答案是:先收缩(把重复字符移出),再更新长度。因为长度计算的依据是当前窗口的边界,如果窗口内还有重复字符,此时计算的长度是无效的。

双指针的核心是"用两个游标在有序性上做文章"。对撞指针依赖数组有序,快慢指针依赖步长差异,分离指针则分别处理不同的维度。这三者的共同前提是:指针移动方向和数据的有序性之间存在可证明的单调关系。

单调栈的底层逻辑是"维持一个单调序列,这个序列的每个元素代表一个候选答案"。当新元素破坏单调性时,被弹出的元素就找到了"下一个更大/更小"的位置关系。这个"弹出即是找到答案"的特性,是单调栈之所以能 O(n) 解决区间极值问题的根本原因。

三、生产级代码实现与最佳实践:三个模板的工程化封装

""" 高频算法模板库 —— 滑动窗口、双指针、单调栈 设计目标:每一个模板都是可直接复用的工程代码,而非竞赛风格的极简实现 每个模板包含:核心逻辑 + 边界处理 + 时空复杂度注释 """ from collections import Counter, deque from typing import List # ========== 模板一:可变滑动窗口 ========== def longest_substring_with_k_distinct(s: str, k: int) -> int: """ LeetCode 340:最多包含 K 个不同字符的最长子串 时间复杂度:O(n),每个字符最多被加入和移除各一次 空间复杂度:O(k),哈希表仅存储 k 种字符的计数 设计要点: 1. 窗口用左右指针 [left, right) 表示,这是最常见的约定 2. 计数用 Counter,它是 dict 的子类,在 O(1) 内完成增减 3. 收缩条件在扩张之后判断,保证窗口状态始终有效 """ if k == 0 or not s: return 0 # 边界:无字符或 K=0 时直接返回 counter: Counter[str] = Counter() # 当前窗口内的字符计数 left = 0 max_len = 0 for right, ch in enumerate(s): counter[ch] += 1 # 扩张窗口:总是先将字符纳入窗口 # 收缩条件:不同字符数超过 K # 注意这里是 while 而非 if,因为可能需要多次收缩 while len(counter) > k: left_char = s[left] counter[left_char] -= 1 if counter[left_char] == 0: # 计数归零时必须删除 key,否则 len(counter) 不会减少 del counter[left_char] left += 1 # 此时窗口内不同字符数 ≤ K,更新最大长度 # 更新时机必须在收缩之后,保证窗口有效 max_len = max(max_len, right - left + 1) return max_len # ========== 模板二:快慢指针(环形检测) ========== def find_duplicate(nums: List[int]) -> int: """ LeetCode 287:寻找重复数(Floyd 判圈算法) 时间复杂度:O(n) 空间复杂度:O(1),不使用额外空间 核心思想:将数组视为链表,值代表 next 指针指向的下标 如果有重复数,链表中必然存在环 slow 每次走一步,fast 每次走两步,相遇后在环内从头同步走 """ # 第一阶段:检测环的存在 slow = fast = nums[0] while True: slow = nums[slow] # 慢指针走一步 fast = nums[nums[fast]] # 快指针走两步 if slow == fast: break # 相遇,确认有环 # 第二阶段:找环的入口(即重复数) slow = nums[0] # 慢指针回到起点 while slow != fast: slow = nums[slow] fast = nums[fast] # 此时 slow/ fast 指向环的入口,即重复数 return slow # ========== 模板三:单调递减栈(下一个更大元素) ========== def daily_temperatures(temperatures: List[int]) -> List[int]: """ LeetCode 739:每日温度 时间复杂度:O(n),每个元素最多入栈出栈各一次 空间复杂度:O(n),栈最多存储 n 个元素 核心技巧:栈中存储下标而非值,通过下标可以同时获取值和位置差 这是单调栈模板最重要的设计选择 """ n = len(temperatures) result = [0] * n # 结果数组,默认 0 表示未找到 stack: List[int] = [] # 单调递减栈(存下标) for i, temp in enumerate(temperatures): # 新元素大于栈顶对应的值 → 弹出栈顶并记录结果 while stack and temp > temperatures[stack[-1]]: prev_idx = stack.pop() # 弹出较小的元素 result[prev_idx] = i - prev_idx # 天数差 # 无论如何都将当前下标入栈 stack.append(i) # 栈中剩余的元素找不到比它更大的温度,result 默认为 0 return result

这三个模板覆盖了 7 月高频题中的核心模式。模板不是用来背的,而是用来理解"为什么这样设计"的。理解了为什么单调栈存下标而非值,你才能应对"循环数组求下一个更大元素"这种变形题。

四、边界分析与架构权衡:什么时候用哪种技巧

一个常见误区是强行套模板。不是所有"求最长"都能用滑动窗口,不是所有"成对比较"都能用双指针。选择的依据是两个关键判断:

第一个判断:问题是否具有"单调性"。滑动窗口要求窗口扩张/收缩的条件是单调的——你不能时而向左时而向右地调整。单调栈要求元素间的比较关系是确定的。"接雨水"能用单调栈,是因为柱子高度的比较结果是确定的。

第二个判断:复杂度目标是否可接受。如果暴力解已经是 O(n),引入复杂技巧没有意义。例如"判断数组是否有重复元素",直接用 set 遍历即可,不需要上双指针。

此外,需要警惕模板的"缝合怪陷阱"。有些题需要滑动窗口 + 单调队列的组合(如滑动窗口最大值),此时两个模板各自独立的部分需要合并。合并的关键在于:用单调队列维护窗口内的单调性,用滑动窗口控制窗口范围。分开理解每个组件,再组合,而不是指望存在一个万能模板。

五、总结

7 月对这三个技巧的精进,核心收获不在代码,而在两个认知上的升级:

第一,"边界条件"不是需要死记的例外情况,而是算法本质的一部分。滑动窗口的收缩条件设计,本质上是"窗口有效性"这个数学定义在代码中的等价表达。

第二,模板的价值在于提炼共性,而不是替代思考。当你画出了状态转移图,写出了不变式,代码其实已经是水到渠成的事了。

8 月,继续用这个思路去攻区间 DP 和状态压缩 DP。不再追求做题量,追求的是每个技巧都能从原理讲到实现,从实现讲到变形。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/27 14:51:27

为什么你的通义千问总是“答非所问”?揭秘92.6%用户忽略的3层上下文管理机制——附自动修复Prompt模板

更多请点击: https://intelliparadigm.com 第一章:为什么你的通义千问总是“答非所问”?揭秘92.6%用户忽略的3层上下文管理机制——附自动修复Prompt模板 当你反复提问却得到泛泛而谈、偏离意图或前后矛盾的回答时,问题往往不在…

作者头像 李华
网站建设 2026/7/27 14:51:23

微服务架构的十大避坑指南——拆分粒度、数据一致性与服务治理

微服务架构的十大避坑指南——拆分粒度、数据一致性与服务治理 一、微服务不是银弹 微服务架构诞生已近十年,在国内互联网行业的落地率很高,但"落地"不等于"落好"。7月份我们参与了三个遗留系统的微服务化改造评审,发现几…

作者头像 李华
网站建设 2026/7/27 14:48:59

3步搞定黑苹果:OpCore-Simplify让OpenCore配置变得如此简单

3步搞定黑苹果:OpCore-Simplify让OpenCore配置变得如此简单 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 你是否对黑苹果配置感到困惑&a…

作者头像 李华
网站建设 2026/7/27 14:47:48

如何使用pgwire快速搭建PostgreSQL协议兼容服务器?5分钟入门教程

如何使用pgwire快速搭建PostgreSQL协议兼容服务器?5分钟入门教程 【免费下载链接】pgwire PostgreSQL wire protocol implemented as a rust library. 项目地址: https://gitcode.com/gh_mirrors/pg/pgwire pgwire是一个用Rust实现的PostgreSQL wire协议库&a…

作者头像 李华
网站建设 2026/7/27 14:47:04

终极无线安装方案:在iPhone上直接安装IPA文件的完整指南

终极无线安装方案:在iPhone上直接安装IPA文件的完整指南 【免费下载链接】App-Installer On-device IPA installer 项目地址: https://gitcode.com/gh_mirrors/ap/App-Installer App-Installer是一款革命性的iOS应用安装工具,让您能够直接在iPhon…

作者头像 李华
网站建设 2026/7/27 14:45:57

解决虚幻引擎版本切换后UnrealBuildTool缺失的完整指南

1. 项目概述:一个看似简单却暗藏玄机的版本切换问题如果你是一名虚幻引擎(Unreal Engine, 简称UE)的开发者,无论是刚入门的新手还是有一定经验的老手,在项目开发过程中,切换引擎版本几乎是一个绕…

作者头像 李华