【数据结构与算法学习笔记】双指针
前言
本文为个人学习的算法学习笔记,学习笔记,学习笔记,不是经验分享与教学,不是经验分享与教学,不是经验分享与教学,若有错误各位大佬轻喷(T^T)。主要使用编程语言为Python3,各类资料题目源于网络,主要自学途径为博学谷,侵权即删。
一、双指针概述
双指针是程序设计中核心优化技巧之一,核心原理是通过两个指针(或索引) 在数据结构(如链表、数组)上协同移动,依据不同移动策略解决特定问题,其核心价值在于将暴力解法的时间复杂度(如 O (n²))优化为线性复杂度(O (n)),且通常仅需 O (1) 额外空间,是竞赛初赛的高频考点之一。
根据指针移动方向的差异,将双指针分为快慢指针和左右指针两类,二者适用场景、移动逻辑存在显著差异,下文将结合课件内容详细拆解。
二、双指针类型一:快慢指针
1. 定义与核心特点
- 移动方向:同向移动(沿数据结构同一方向,如均从链表头向链表尾移动)
- 移动策略:两个指针移动速度不同(如快指针每次走 2 步,慢指针每次走 1 步)或遵循不同触发条件
- 适用场景:主要用于链表,解决 “链表环检测”“特定节点查找”(如链表中间节点、倒数第 K 个节点)等问题
- 核心优势:无需额外存储空间,仅通过 “速度差” 定位目标,效率高且实现简洁
2. 典型例题:LeetCode 141. 环形链表(课件重点案例)
(1)题目背景(课件提及)
给定一个链表的头节点head
,判断链表是否存在环(即某节点的next
指针指向已遍历过的节点,形成闭环),存在则返回true
,否则返回false
。
(2)解题思路(基于课件逻辑)
类比 “环形跑道跑步”:若两人在环形跑道上跑步,速度快者终将追上速度慢者;若为直线跑道,速度快者会先到达终点。基于此设计如下步骤:
- 初始化指针:快慢指针均从链表头节点出发(
slow = head
,fast = head
),为避免初始状态下两指针相等的误判,可让fast
先移动 1 步(fast = head.next
); - 设定移动规则:慢指针每次移动 1 步(
slow = slow.next
),快指针每次移动 2 步(fast = fast.next.next
); - 判断逻辑:
- 若链表无环:快指针会先到达链表尾部(
fast == None
或fast.next == None
),直接返回false
; - 若链表有环:快指针会在环内追上慢指针(
slow == fast
),返回true
。
- 若链表无环:快指针会先到达链表尾部(
(3)Python 代码实现
# 定义链表节点结构(符合课件中链表的节点构成逻辑)
class ListNode:def __init__(self, x):self.val = xself.next = Noneclass Solution:def hasCycle(self, head: ListNode) -> bool:# 边界条件:空链表或仅1个节点,必然无环(课件隐含边界处理逻辑)if not head or not head.next:return False# 初始化快慢指针:快指针先移动1步,避免初始相等误判slow = headfast = head.next# 循环移动指针,直到相遇或快指针到达尾部while slow != fast:# 快指针到达尾部(无环),直接返回falseif not fast or not fast.next:return Falseslow = slow.next # 慢指针走1步fast = fast.next.next # 快指针走2步# 快慢指针相遇,存在环return True
3. 举一反三:快慢指针的其他应用(课件提及场景)
(1)LeetCode 876. 链表的中间结点
- 题目需求:找到非空链表的中间节点,若节点数为偶数,返回第二个中间节点(如链表
1→2→3→4
,返回3
); - 解题思路(课件逻辑):
- 快慢指针同时从
head
出发,快指针每次走 2 步,慢指针每次走 1 步; - 当快指针到达尾部(
fast == None
或fast.next == None
)时,慢指针恰好指向中间节点;
- 快慢指针同时从
- Python 代码实现:
class Solution:def middleNode(self, head: ListNode) -> ListNode:slow = fast = head# 快指针未到尾部时,持续移动while fast and fast.next:slow = slow.next # 慢指针1步fast = fast.next.next # 快指针2步# 快指针到达尾部,慢指针指向中间节点return slow
(2)查找链表的倒数第 K 个节点(课件拓展场景)
- 题目需求:给定链表,找到从末尾数第 K 个节点(如链表
1→2→3→4→5
,K=2,返回4
); - 解题思路(课件逻辑):
- 快指针先出发,向前移动 K 步(与慢指针拉开 K 个节点的距离);
- 快慢指针再同时以 1 步 / 次的速度移动;
- 当快指针到达尾部(
fast == None
)时,慢指针指向的即为倒数第 K 个节点;
- Python 代码实现:
class Solution:def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:slow = fast = head# 快指针先移动K步for _ in range(k):if not fast: # 若K大于链表长度,返回None(边界处理)return Nonefast = fast.next# 快慢指针同时移动,直到快指针到达尾部while fast:slow = slow.nextfast = fast.nextreturn slow
4. 快慢指针小结(基于课件内容)
- 核心逻辑:通过 “速度差” 制造指针间的固定距离或相对运动,从而定位环、中间节点等目标;
- 关键注意事项:
- 边界处理:需避免快指针访问
None.next
(需判断fast
和fast.next
是否为None
); - 初始位置:根据题目需求调整(如环形链表需让快指针先移动 1 步,避免初始误判)。
- 边界处理:需避免快指针访问
三、双指针类型二:左右指针
1. 定义与核心特点
- 移动方向:相向移动(从数据结构两端向中间移动,如左指针从数组起始索引 0 出发,右指针从数组末尾索引
len(arr)-1
出发); - 移动策略:根据题目条件判断移动左指针或右指针,直到两指针相遇(
left >= right
); - 适用场景:主要用于数组(尤其有序数组),解决 “盛最多水的容器”“两数之和”“区间查找” 等问题;
- 核心优势:将暴力枚举的 O (n²) 时间复杂度降至 O (n),且无需额外空间。
2. 典型例题:LeetCode 11. 盛最多水的容器(课件重点案例)
(1)题目背景(课件提及)
给定非负整数数组height
,每个元素代表柱子高度(横坐标为索引,纵坐标为高度),选择两根柱子与 x 轴组成容器,求容器能容纳的最大水量,水量计算公式为W(i,j) = Min(height[i], height[j]) * (j-i)
(i<j
)。
(2)解题思路(基于课件逻辑)
容器水量由 “最短柱子高度” 和 “柱子间距” 共同决定,核心是通过 “两端收缩” 寻找最优组合:
- 初始化指针:左指针
left=0
(数组起始),右指针right=len(height)-1
,记录最大水量max_water=0
; - 计算当前水量:按公式计算
current_water = min(height[left], height[right]) * (right - left)
,若current_water > max_water
,则更新max_water
; - 指针移动规则(课件关键结论):
- 若
height[left] < height[right]
:移动左指针(left += 1
)—— 此时容器高度由左柱子决定,移动右指针会减小间距且无法提升高度,水量必然减少;移动左指针可能遇到更高柱子,有机会提升水量; - 若
height[left] >= height[right]
:移动右指针(right -= 1
),逻辑同上;
- 若
- 循环终止:当
left >= right
时,遍历结束,返回max_water
。
(3)Python 代码实现
class Solution:def maxArea(self, height: list[int]) -> int:left = 0right = len(height) - 1max_water = 0while left < right:# 计算当前水量(按课件公式W(i,j)计算)current_height = min(height[left], height[right])current_width = right - leftcurrent_water = current_height * current_width# 更新最大水量if current_water > max_water:max_water = current_water# 按课件规则移动指针:移动较矮的柱子对应的指针if height[left] < height[right]:left += 1else:right -= 1return max_water
3. 举一反三:左右指针的其他应用(课件提及场景)
LeetCode 167. 两数之和 II - 输入有序数组
- 题目需求:给定非递减排序的数组
numbers
和目标值target
,找出两个数使其和为target
,返回两数的索引(从 1 开始计数,答案唯一); - 解题思路(课件逻辑):利用数组有序特性,通过 “两端收缩” 缩小范围:
- 初始化
left=0
,right=len(numbers)-1
; - 计算当前和
sum = numbers[left] + numbers[right]
:- 若
sum == target
:返回[left+1, right+1]
(索引从 1 开始,符合题目要求); - 若
sum < target
:移动左指针(left += 1
)—— 需更大的数补充和; - 若
sum > target
:移动右指针(right -= 1
)—— 需更小的数减少和;
- 若
- 初始化
- Python 代码实现:
class Solution:def twoSum(self, numbers: list[int], target: int) -> list[int]:left = 0right = len(numbers) - 1while 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 [] # 题目保证有答案,此句仅为语法完整性
4. 左右指针小结(基于课件内容)
- 核心逻辑:通过 “两端收缩” 缩小查找范围,利用数组有序性或问题特性(如柱子高度对比)优化效率;
- 关键注意事项:
- 适用前提:数组有序(如两数之和 II)或问题与两端状态直接相关(如盛水容器);
- 移动依据:明确 “移动哪个指针” 的判断条件,避免无效移动(如盛水容器需移动较矮柱子的指针)。
四、双指针整体总结(基于课件内容)
1. 两类双指针对比表
指针类型 | 移动方向 | 核心适用场景 | 时间复杂度 | 空间复杂度 | 关键技巧 | 课件对应段落 |
---|---|---|---|---|---|---|
快慢指针 | 同向 | 链表环检测、中间节点、倒数第 K 节点 | O(n) | O(1) | 控制指针速度差,制造相对距离 | |
左右指针 | 相向 | 数组盛水、有序数组两数之和 | O(n) | O(1) | 依据条件收缩两端范围,优化效率 |
2. 复习建议(结合课件备考需求)
- 抓核心逻辑:重点掌握快慢指针的 “速度差”、左右指针的 “两端收缩”,结合课件例题理解背后的设计思路(如盛水容器为何移动较矮指针);
- 练典型真题:优先练习课件提及的 LeetCode 题目(141、876、11、167),这些是 “传智杯” 初赛双指针题型的 “原型题”,竞赛常在此基础上变形;
- 强边界处理:始终关注指针越界问题(如快指针的
None
判断、数组指针的left < right
循环条件),避免代码报错。