双指针入门实战:从「移动零」到「盛最多水的容器」------我在 Python 中理解"指针"的旅程
关键词 :双指针|移动零(LeetCode #283)|盛最多水的容器(LeetCode #11)|Python 指针思想|原地操作
感悟 :Python 的"指针"不像 C 那样需要*和&,而是通过变量直接引用下标或对象,简洁而强大。
一、引子:我对"指针"的重新认识
刚开始接触算法题时,总以为"指针"是 C/C++ 的专属概念。直到刷了「移动零」和「盛最多水的容器」这两道题,我才恍然大悟:
✅ 在 Python 中,指针 = 用变量表示数组下标或对象引用
✅ 双指针 = 两个变量分别指向数组的不同位置,协同工作
不需要 int* p,不需要取地址 &,只需要:
python
left = 0
right = len(arr) - 1
这两个变量,就是我的"指针"。
二、第一题:移动零(LeetCode #283)
🔍 题目要求
将数组中所有 0 移动到末尾,保持非零元素的相对顺序 ,原地修改。
💡 我的解法思路
我采用两阶段双指针策略:
- 第一遍 :用
fast遍历数组,slow记录"下一个非零应放的位置"- 遇到非零 →
nums[slow] = nums[fast],然后slow += 1
- 遇到非零 →
- 第二遍 :从
slow到末尾,全部设为0
✅ 我的代码
python
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow] = nums[fast]
slow += 1
for i in range(slow, len(nums)):
nums[i] = 0
🌟 我的收获 :
这不是传统指针,而是用整数变量模拟指针行为 ------
slow和fast就是两个"下标指针"。
🔍 官方解法:一次遍历 + 交换
官方代码更精炼,只用一次遍历:
python
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
left = right = 0
while right < len(nums):
if nums[right] != 0:
nums[left], nums[right] = nums[right], nums[left] # ← 关键!
left += 1
right += 1
❓ 我的疑问:为什么交换可行?
起初我不理解这行交换的逻辑。后来画图才明白:
-
等价于:
python# 临时保存右边的值(Python 自动完成) temp1 = nums[right] temp2 = nums[left] nums[left] = temp1 nums[right] = temp2✅ 效果就是:交换
nums[left]和nums[right]的值
✅ 本质和我的方法一样,只是用交换避免了第二次循环补 0,更高效!
关键洞察:为什么 nums[left] 一定是 0?
这是理解整个算法的核心!
🤔 观察 left 的含义:
left始终指向下一个应该放非零元素的位置- 而且,在
left左侧([0, left))已经全是非零元素了
那么,left 当前位置是什么?
- 因为数组中只有 0 和非零,
- 而非零都已经被"处理"到左边了,
- 所以
nums[left]要么是 0,要么还没被访问(但初始是 0 或会被覆盖)
👉 在我们的例子中:
- 第一次交换前:
left=0,nums[0]=0 - 第二次交换前:
left=1,nums[1]=0(因为上一步把 1 换到了位置 0,0 被换到位置 1) - 第三次交换前:
left=2,nums[2]=0
✅ 所以,每次交换都是:把一个非零(nums[right])和一个 0(nums[left])互换位置
这就实现了:
- 非零元素逐步移到前面
- 0 被"挤"到后面
三、第二题:盛最多水的容器(LeetCode #11)
🔍 题目要求
给定 n 个非负整数 height[i] 表示竖线高度,找出两条线,使得它们与 x 轴构成的容器装水最多。
面积公式:area = (right - left) * min(height[left], height[right])
❌ 我的暴力解法(超时)
python
class Solution:
def maxArea(self, height: List[int]) -> int:
max_area = 0
n = len(height)
for short in range(n-1):
long = short + 1
while long != n:
current_area = (long - short) * min(height[short], height[long])
max_area = max(max_area, current_area)
long += 1
return max_area
- 时间复杂度:O(n²)
- 对于
n=10⁵的数据,必然超时 ❌
✅ 我的双指针优化解法
我意识到:面积由"宽度"和"短板"决定 。于是采用左右指针向中间收缩的策略:
- 初始化:
slow = 0(左),fast = len(height)-1(右) - 每次计算当前面积
- 移动较短的那一边(因为移动长边不会增加面积,只有换掉短板才可能更大)
✅ 我的代码
python
class Solution:
def maxArea(self, height: List[int]) -> int:
slow = 0
fast = len(height) - 1
max_area = 0
while slow < fast:
if height[slow] <= height[fast]:
current_area = (fast - slow) * height[slow]
slow += 1
else:
current_area = (fast - slow) * height[fast]
fast -= 1
max_area = max(max_area, current_area)
return max_area
🌟 关键洞察 :
移动短板,才有机会找到更大的面积。这是贪心 + 双指针的经典结合!
- 时间复杂度:O(n)
- 空间复杂度:O(1)
深入理解:Python 的"引用"机制
🔑 关键概念:
- 一切皆对象
- 变量存储的是对象的引用(内存地址的抽象)
- 赋值 = 让变量指向同一个对象
🧪 示例 1:可变对象(如 list、自定义类)
python
a = [1, 2, 3]
b = a # b 和 a 指向同一个 list 对象
b.append(4)
print(a) # [1, 2, 3, 4] → a 也被改变了!
✅ 这就像两个指针指向同一块内存。
🧪 示例 2:不可变对象(如 int、str、tuple)
python
x = 10
y = x
y = 20
print(x) # 10 → x 不变
❌ 因为 int 不可变,y = 20 是让 y 指向新对象,不影响 x。
💡 所以:只有可变对象才能实现"通过一个引用修改,另一个看到变化"的指针效果。
四、我的核心收获总结
| 维度 | 收获 |
|---|---|
| Python 指针思想 | 不需要显式指针,用变量表示下标就是指针 |
| 双指针模式 | 两个变量协同工作: • 移动零:slow(写入位)、fast(读取位) • 盛水容器:left、right(两端向中间) |
| 原地操作技巧 | 通过覆盖或交换,避免新建数组 |
| 算法优化意识 | 从 O(n²) 暴力 → O(n) 双指针,理解"为什么能优化"比记住代码更重要 |
| 代码风格 | 变量命名清晰(slow/fast 或 left/right)让逻辑一目了然 |
五、两类双指针模式对比
| 题型 | 指针移动方式 | 典型场景 |
|---|---|---|
| 同向双指针 | slow 和 fast 从左往右 |
移动零、删除重复项、滑动窗口 |
| 相向双指针 | left 从左,right 从右,向中间靠拢 |
盛最多水、两数之和 II、回文判断 |
💡 口诀 :
"同向压缩,相向试探"