02 · 双指针与滑动窗口

来源: labuladong 双指针框架 + 滑动窗口”三问法” + 代码随想录双指针专题
核心价值: 将 O(n²) 暴力解优化到 O(n)


一、本质理解

双指针的核心思想是 利用两个指针的相对运动来减少搜索空间

labuladong 的双指针分类:

双指针
├── 左右指针(对撞):两端向中间移动 → 有序数组、回文串
├── 快慢指针(同向):一快一慢 → 链表判环、找中点
└── 滑动窗口(同向):维护一个区间 → 子串/子数组问题

延伸理解: 滑动窗口本质上是 快慢指针的一种特殊形式,特殊在窗口内的元素始终是连续的(子数组/子串)。


二、对撞指针(左右指针)

核心思路

两个指针分别从数组的两端出发,向中间移动,直到相遇。

适用条件: 数组通常是有序的(或经过排序处理)。

标准模板

def two_sum_sorted(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        s = nums[left] + nums[right]
        if s == target:
            return [left, right]
        elif s < target:
            left += 1    # 和太小,左指针右移
        else:
            right -= 1   # 和太大,右指针左移
    return []

三数之和(排序 + 对撞指针)

def three_sum(nums):
    nums.sort()
    n = len(nums)
    res = []
    
    for i in range(n - 2):
        # 去重:跳过重复的固定元素
        if i > 0 and nums[i] == nums[i - 1]:
            continue
        # 双指针找两数之和
        left, right = i + 1, n - 1
        while left < right:
            s = nums[i] + nums[left] + nums[right]
            if s == 0:
                res.append([nums[i], nums[left], nums[right]])
                left += 1
                right -= 1
                # 去重:跳过重复元素
                while left < right and nums[left] == nums[left - 1]:
                    left += 1
                while left < right and nums[right] == nums[right + 1]:
                    right -= 1
            elif s < 0:
                left += 1
            else:
                right -= 1
    return res

三、快慢指针

核心思路

两个指针从同一起点出发,一个走的快(如每次 2 步),一个走的慢(如每次 1 步)。

模板 1:链表判环

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

模板 2:找环入口(Floyd 算法)

def detect_cycle(head):
    slow = fast = head
    # 第一阶段:快慢指针相遇
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            break
    else:  # 无环
        return None
    
    # 第二阶段:头指针和慢指针同步走,相遇点即为入口
    slow = head
    while slow != fast:
        slow = slow.next
        fast = fast.next
    return slow

模板 3:找链表中点

def middle_node(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow  # 偶数长度时返回偏右的中点

四、滑动窗口 ⭐(高频核心)

labuladong 三问判断法

滑动窗口的核心是 三个问题

问题一:什么时候扩大窗口?
→ 当前窗口还不满足"可行解"条件时,右指针右移扩大

问题二:什么时候缩小窗口?
→ 当前窗口已经满足"可行解"条件时,左指针右移缩小

问题三:什么时候更新答案?
→ 缩小窗口时(求最小)或扩大窗口时(求最大)

通用模板

from collections import defaultdict
 
def sliding_window(s):
    left, right = 0, 0
    window = defaultdict(int)  # 窗口内元素统计
    # need = ...  # 目标条件(看题目需要)
    
    while right < len(s):
        # 扩大窗口:右指针右移
        c = s[right]
        right += 1
        window[c] += 1
        # ... 更新窗口数据 ...
        
        # 缩小窗口:当窗口满足条件时
        while "窗口需要缩小":
            d = s[left]
            left += 1
            window[d] -= 1
            if window[d] == 0:
                del window[d]
            # ... 更新窗口数据 ...
    
    return result

模板应用:无重复字符的最长子串

def length_of_longest_substring(s):
    window = defaultdict(int)
    left = right = 0
    max_len = 0
    
    while right < len(s):
        c = s[right]
        right += 1
        window[c] += 1
        
        # 出现重复字符,缩小窗口
        while window[c] > 1:
            d = s[left]
            left += 1
            window[d] -= 1
        
        # 扩大窗口时更新答案(求最大)
        max_len = max(max_len, right - left)
    
    return max_len

模板应用:最小覆盖子串

from collections import defaultdict
 
def min_window(s, t):
    need = defaultdict(int)
    window = defaultdict(int)
    for c in t:
        need[c] += 1
    
    left = right = 0
    valid = 0  # 已满足需求的字符种类数
    start, length = 0, float('inf')
    
    while right < len(s):
        c = s[right]
        right += 1
        
        if c in need:
            window[c] += 1
            if window[c] == need[c]:
                valid += 1
        
        # 所有字符都已覆盖,开始收缩
        while valid == len(need):
            # 缩小窗口时更新答案(求最小)
            if right - left < length:
                start = left
                length = right - left
            
            d = s[left]
            left += 1
            if d in need:
                if window[d] == need[d]:
                    valid -= 1
                window[d] -= 1
    
    return s[start:start + length] if length != float('inf') else ""

五、Hot 100 双指针/滑动窗口题目清单

双指针(对撞)

题号题目难度核心思路建议用时
283移动零Easy快慢指针,非零前移25 min
11盛最多水的容器Medium对撞指针,移动较短的30 min
15三数之和Medium排序 + 对撞指针 + 去重40 min
42接雨水Hard对撞指针,维护左右最大高40 min

滑动窗口

题号题目难度核心思路建议用时
3无重复最长子串Medium滑动窗口 + 哈希集30 min
438找所有字母异位词Medium定长滑动窗口 + 计数35 min
239滑动窗口最大值Hard单调递减队列40 min
76最小覆盖子串Hard滑动窗口 + 需求计数45 min

六、易错点与技巧

  1. 滑动窗口 vs 双指针:滑动窗口解决”连续子区间”问题,双指针解决”两个元素的关系”问题
  2. 滑动窗口的边界right - left 是窗口长度(右开区间),right - left + 1 是闭区间长度
  3. 去重技巧:三数之和中的去重用 while 跳过相邻重复值
  4. 接雨水的核心:当前位置能接的水 = min(左边最高, 右边最高) - 当前高度
  5. 单调队列:滑动窗口最大值用双端队列 collections.deque,维护递减顺序

七、复杂度总结

模式时间复杂度空间复杂度
对撞指针O(n)O(1)
快慢指针O(n)O(1)
滑动窗口O(n)O(k),k 为字符集大小
排序 + 双指针O(n log n)O(1) 或 O(n)

八、参考与延伸