最近在给团队做算法面试辅导,发现一个特别有意思的现象:LeetCode 283 这道“移动 0 的位置”几乎是人人都刷过的题,但每次让我看候选人现场写代码,十个人里有七八个会写出不太干净的版本。要么边界条件处理得拖泥带水,要么没意识到这题考察的其实是双指针里最基础的“读写指针”思想。
这篇文章把这道题从头到尾彻底拆一遍,包括暴力解法的局限、双指针的演进过程、我见过的一个反直觉的边界 bug,以及它在实际业务场景里的变体应用。适合刚入门算法的朋友建立思维框架,也适合准备面试的同学查漏补缺。
1. 问题的本质:一条看似简单但暗藏陷阱的数组操作题
题目描述直白得不像个中等题:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。必须原地操作,不能拷贝额外数组。
我见过最典型的错误理解是:这题不就是排序吗?把 0 全排到后面去。这么想也不能说全错,但很容易引导出错误的解法。因为题目要求“保持非零元素的相对顺序”,排序恰恰会破坏这个约束。
举个例子,[1, 0, 3, 12],你要的结果是 [1, 3, 12, 0],不是 [12, 3, 1, 0]。这意味着你只能移动 0,不能重排非零元素之间原本的先后关系。这个约束条件一加,排序思路基本就废了。
换个角度重新理解这个问题:我们不是在“移动 0”,而是在“搬运非零元素到前面”,剩下的坑位自动就是 0。 这句话看着像文字游戏,实际上是两种完全不同的实现路径。
- 如果按“移动 0”来想,你需要考虑 0 被交换到后面之后,后续遍历要不要跳过、会不会重复处理。
- 如果按“搬运非零元素”来想,思路就清晰多了:从头到尾扫描一遍,遇到非零的就往前面放,扫描完了,后面全部补 0。
很多人觉得这题简单,但真正动手写的时候才发现,双指针的退出条件、交换逻辑、补 0 的位置,每个环节都有出错的余地。
提示:这道题的通用叫法是 Move Zeroes,在 LeetCode 上是第 283 题,属于“数组 + 双指针”的经典入门题。它的变体在字符串压缩、磁盘碎片整理、日志文件归档等场景里都有体现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从朴素解法到双指针:一趟遍历背后的思考链路
2.1 先承认一个事实:暴力解法不是不能用
我第一次刷这道题的时候,直接写了个两层循环:外层遍历数组,找到一个 0 之后,内层循环往右找下一个非零元素,然后交换。代码跑通了,但总感觉有点不对劲。
python复制def move_zeroes_naive(nums):
n = len(nums)
i = 0
while i < n:
if nums[i] == 0:
j = i + 1
while j < n and nums[j] == 0:
j += 1
if j < n:
nums[i], nums[j] = nums[j], nums[i]
i += 1
return nums
这个解法最坏情况是 [0, 0, 0, ..., 1]——所有的 0 都在前面,只有最后一个元素是 1。这时内层循环每次都要扫到数组末尾,时间复杂度退化到 O(n²),空间复杂度 O(1)。
为什么 O(n²) 不能接受? 如果你在真实业务里处理的是 10 万条日志里夹杂着 3 万个空字段,O(n²) 意味着十亿次级别的操作,这在客户端或网关层几乎必然导致卡顿。这也是面试官大概率会追问的点:能优化到 O(n) 吗?
2.2 双指针的两种经典写法:前后指针 vs 快慢指针
既然暴力解法慢在“每次找非零元素都要重新扫描”,那自然想到用指针缓存住已经扫描过的位置。这就是双指针的由来。
写法一:快慢指针(a.k.a. 读写指针)
这个写法的核心思路是:维护一个 left 指针指向“下一个应该放置非零元素的位置”,用 right 指针遍历整个数组。当 right 指向的元素非零时,把它复制到 left 位置,left 右移一位。遍历结束后,left 之后的所有位置补 0。
``
