200字
【双指针】入门技巧
2025-12-14
2025-12-14

双指针算法详解

一、双指针算法的核心含义

双指针是一种通过使用两个指针(索引)在数据结构(通常是数组或链表)中同时遍历的技巧。这两个指针可以:

  1. 从同一端出发,但以不同的速度移动(快慢指针)

  2. 从两端向中间移动(左右指针)

  3. 从不同位置出发,分别扫描

二、双指针算法的类型与使用技巧

1. 快慢指针

  • 特点:两个指针从同一起点出发,但移动速度不同

  • 应用场景

    • 链表中的环检测

    • 寻找链表中点

    • 寻找链表的第k个节点

  • 技巧

    • 快指针通常移动速度为慢指针的2倍

    • 注意边界条件处理

# 快慢指针模板
slow, fast = start, start
while fast and fast.next:
    slow = slow.next      # 移动一步
    fast = fast.next.next # 移动两步
    # 根据具体问题处理

2. 左右指针(对撞指针)

  • 特点:一个指针从起点开始,另一个从终点开始,向中间移动

  • 应用场景

    • 有序数组的两数之和

    • 反转数组

    • 回文串判断

  • 技巧

    • 通常用于已排序的数组

    • 根据比较结果决定移动哪个指针

# 左右指针模板
left, right = 0, len(array)-1
while left < right:
    if condition1:
        left += 1
    elif condition2:
        right -= 1
    else:
        # 处理结果
        left += 1
        right -= 1

3. 滑动窗口

  • 特点:两个指针维护一个窗口,根据条件动态调整窗口大小

  • 应用场景

    • 子串/子数组问题

    • 最长不重复子串

    • 最小覆盖子串

  • 技巧

    • 通常右指针扩展窗口,左指针收缩窗口

    • 使用哈希表辅助记录窗口内状态

python

# 滑动窗口模板
left = 0
for right in range(len(array)):
    # 更新窗口状态(添加right指向的元素)
    
    while 窗口不满足条件:
        # 更新窗口状态(移除left指向的元素)
        left += 1
    
    # 更新结果

三、应用模板

模板1:双向遍历

def two_pointers_template1(nums):
    left, right = 0, len(nums) - 1
    result = []
    
    while left < right:
        # 根据条件处理
        if condition(nums, left, right):
            # 记录结果
            result.append(...)
            left += 1
            right -= 1
        elif nums[left] < nums[right]:
            left += 1
        else:
            right -= 1
    
    return result

模板2:快慢指针

def two_pointers_template2(head):
    slow = fast = head
    
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        
        if slow == fast:  # 检测环
            return True
    
    return False

模板3:滑动窗口

def sliding_window_template(s, target):
    left = 0
    window = {}
    result = 0
    
    for right in range(len(s)):
        # 更新右边界
        window[s[right]] = window.get(s[right], 0) + 1
        
        # 收缩左边界直到满足条件
        while 不满足条件(window):
            window[s[left]] -= 1
            if window[s[left]] == 0:
                del window[s[left]]
            left += 1
        
        # 更新结果
        result = max(result, right - left + 1)
    
    return result

四、LeetCode实例

1. 两数之和 II(左右指针)

def twoSum(numbers, target):
    left, right = 0, len(numbers) - 1
    
    while left < right:
        current_sum = numbers[left] + numbers[right]
        
        if current_sum == target:
            return [left + 1, right + 1]  # 题目要求索引从1开始
        elif current_sum < target:
            left += 1
        else:
            right -= 1
    
    return []

2. 判断链表是否有环(快慢指针)

def hasCycle(head):
    if not head or not head.next:
        return False
    
    slow, fast = head, head
    
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        
        if slow == fast:
            return True
    
    return False

3. 最长无重复字符子串(滑动窗口)

def lengthOfLongestSubstring(s):
    char_index = {}  # 存储字符最后出现的位置
    left = 0
    max_length = 0
    
    for right in range(len(s)):
        if s[right] in char_index:
            # 更新左指针,确保不重复
            left = max(left, char_index[s[right]] + 1)
        
        char_index[s[right]] = right
        max_length = max(max_length, right - left + 1)
    
    return max_length

4. 盛最多水的容器(左右指针)

def maxArea(height):
    left, right = 0, len(height) - 1
    max_water = 0
    
    while left < right:
        # 计算当前容量
        width = right - left
        current_height = min(height[left], height[right])
        current_water = width * current_height
        
        max_water = max(max_water, current_water)
        
        # 移动较短的那一边
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1
    
    return max_water

5. 三数之和(左右指针结合)

def threeSum(nums):
    nums.sort()
    result = []
    
    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i-1]:
            continue  # 跳过重复元素
        
        left, right = i + 1, len(nums) - 1
        
        while left < right:
            current_sum = nums[i] + nums[left] + nums[right]
            
            if current_sum == 0:
                result.append([nums[i], nums[left], nums[right]])
                
                # 跳过重复元素
                while left < right and nums[left] == nums[left + 1]:
                    left += 1
                while left < right and nums[right] == nums[right - 1]:
                    right -= 1
                
                left += 1
                right -= 1
            elif current_sum < 0:
                left += 1
            else:
                right -= 1
    
    return result

五、常见问题与优化技巧

1. 指针移动条件

  • 明确何时移动左指针,何时移动右指针

  • 避免死循环:确保至少有一个指针在每次迭代中移动

2. 边界条件处理

  • 空数组或空链表

  • 单个元素的情况

  • 指针越界检查

3. 时间复杂度优化

  • 双指针通常将O(n²)暴力解法优化为O(n)

  • 结合哈希表可以进一步优化某些问题

4. 空间复杂度

  • 多数双指针解法空间复杂度为O(1)

  • 滑动窗口可能需要额外数据结构记录状态

六、总结

双指针是一种强大而灵活的算法技巧,特别适合处理数组链表相关的问题。掌握不同类型的双指针模式(快慢指针、左右指针、滑动窗口)并能根据问题特点选择合适的模式,是解决许多LeetCode问题的关键。

核心要点

  1. 识别问题是否适合双指针(通常是涉及顺序遍历、寻找特定模式或子结构的问题)

  2. 根据问题特点选择合适的指针移动策略

  3. 注意指针移动条件和边界情况

  4. 结合其他数据结构(如哈希表)可以解决更复杂的问题

双指针使用口诀与场景判断

一、核心口诀

"三看一选"口诀

一看结构:数组链表排序否?
二看目标:找位置、找范围、找关系?
三看移动:单向走、双向碰、变速跑?
一选模式:定下指针类型后开搞!


二、具体场景判断流程图

text

问题来了
  ↓
问自己:数据结构是什么?
  ↓
├─ 数组 → 是否排序?
│      ├─ 是 → 找两数/三数之和? → 左右指针
│      ├─ 是 → 找范围/区间? → 滑动窗口
│      └─ 否 → 需要原地修改? → 快慢指针
│
├─ 链表 → 什么问题?
│      ├─ 环检测/找中点 → 快慢指针
│      ├─ 反转/重排 → 左右指针
│      └─ 删除节点 → 快慢指针
│
└─ 字符串 → 什么问题?
       ├─ 回文判断 → 左右指针
       ├─ 子串问题 → 滑动窗口
       └─ 字符替换 → 快慢指针

三、场景速查表

左右指针(对撞指针)使用场景

口诀"有序数组两头找,回文反转对对碰"

适用场景

  1. 有序数组找两数:两数之和、三数之和、最接近的三数之和

  2. 反转类问题:反转数组、反转字符串、反转链表

  3. 回文判断:验证回文串、最长回文子串

  4. 容器盛水:盛最多水的容器

  5. 划分数组:颜色分类(荷兰国旗问题)

不适用

  • ❌ 无序且不能排序的数组

  • ❌ 需要保持原始顺序的问题

快慢指针使用场景

口诀"链表环和中点,原地修改慢等快"

适用场景

  1. 链表环问题:检测环、找环起点

  2. 链表中点:找中点、判断回文链表

  3. 原地修改:删除有序数组重复项、移动零

  4. 找倒数第N个:删除链表倒数第N个节点

  5. 数组去重:保留K个相同元素

不适用

  • ❌ 需要两端同时处理的问题

  • ❌ 需要记录窗口内状态的问题

滑动窗口使用场景

口诀"子串子数组长,窗口滑动记状态"

适用场景

  1. 子串/子数组问题:最长无重复子串、最小覆盖子串

  2. 连续区间问题:长度最小的子数组、乘积小于K的子数组

  3. 频率统计问题:找到字符串中所有字母异位词

  4. K个连续问题:最大连续1的个数 III

不适用

  • ❌ 不要求连续性的问题

  • ❌ 不需要维护区间状态的问题


四、决策树 - 问自己这几个问题

问题1:需要同时从两端向中间处理吗?

  • 左右指针

    • 例:有序数组的两数之和

  • → 进入问题2

问题2:需要处理连续的子数组/子串吗?

  • 滑动窗口

    • 例:最长无重复字符子串

  • → 进入问题3

问题3:是链表问题或需要不同速度遍历吗?

  • 快慢指针

    • 例:链表中环检测

  • → 进入问题4

问题4:需要原地修改数组且保持相对顺序吗?

  • 快慢指针

    • 例:删除排序数组中的重复项

  • → 可能不适合双指针,考虑其他算法

常见模式速记

关键词

双指针类型

例题

有序数组、两数之和

左右指针

#167, #15

连续子串、最长/最短子数组

滑动窗口

#3, #209

链表环、链表中点

快慢指针

#141, #876

原地修改、删除重复项

快慢指针

#26, #283

反转、回文

左右指针

#344, #125

盛水、容器

左右指针

#11


六、特别提醒与陷阱

容易混淆的场景

场景1:找"和为目标值的子数组"

  • 如果是有序数组左右指针(#167)

  • 如果是无序数组且要求连续滑动窗口(但注意:滑动窗口通常要求非负或单调)

  • 如果是无序数组且不要求连续 → 不是双指针问题,用哈希表

场景2:删除元素

  • 如果保持原有顺序快慢指针(#27)

  • 如果不要求顺序左右指针(更高效)

场景3:字符串匹配

  • 如果需要连续匹配滑动窗口

  • 如果只需要首尾比较左右指针


七、一句话总结

"有序求和用左右,链表环中用快慢,连续子串用滑动,原地修改快慢走。"

记忆小贴士

  • 左右指针像两个人从两头走向中间

  • 快慢指针像龟兔赛跑

  • 滑动窗口像摄像机在轨道上移动拍摄

最后检查清单

  1. 数据结构是否支持索引访问(数组、链表)?

  2. 是否需要同时考虑两个位置的关系?

  3. 解空间是否可以通过指针移动系统地遍历?

  4. 是否有更简单的解法?(有时哈希表更简单)

记住:双指针的核心思想是通过巧妙的指针移动减少不必要的遍历,将O(n²)优化到O(n)。当发现暴力解法中有大量重复计算时,想想能否用双指针优化!

【双指针】入门技巧
作者
Shisuiyi
发表于
2025-12-14
License
CC BY-NC-SA 4.0

评论