三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

LeetCode 283:移动零(双指针问题) —— 题解

LeetCode 283:移动零(双指针问题) —— 题解

👋 欢迎阅读

一.题目

283. 移动零 - 力扣(LeetCode)

🎯 欢迎来到「移动零」题解之旅!本文将带你从“将所有零移到数组末尾,同时保持非零元素顺序”这一数组操作问题出发,深入理解双指针(快慢指针)的经典应用,并掌握如何原地修改数组,实现高效的一次遍历。

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 283 题,给定一个数组nums,要求将所有0移动到数组末尾,并保持非零元素的相对顺序不变,且必须原地操作(不能复制新数组)。这是数组操作中的基础题,也是双指针思想的入门经典。

  • 明确学习目标:掌握双指针解法——维护一个“慢指针”指向已处理好的非零序列的末尾,用“快指针”遍历数组,遇到非零元素则交换(或覆盖)到慢指针位置,最后将剩余位置填零。理解为什么这种“只关心非零元素,遇到零就跳过”的策略能保持相对顺序,并熟练处理边界(如全零数组或全非零数组)。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如nums = [0,1,0,3,12]输出[1,3,12,0,0])。

本文将从问题转化、双指针策略设计(快慢指针详解)、代码模拟到复杂度分析,层层递进。即使你对双指针还不熟悉,我们也会从“用慢指针记录非零元素应该放的位置”这一直觉出发,让你轻松抓住核心思想——快指针负责探路,慢指针负责记录,所有非零元素依次往前靠,零自然被挤到后面。现在,让我们一起把零“搬运”到末尾,让数组焕然一新吧! 🔄📦

二.做题思路

一、问题分析(前置分析)

给定一个数组nums,要求将所有 0 移动到数组末尾,同时保持非零元素的相对顺序不变。必须原地修改,不能复制数组。
核心观察:等价于将所有非零元素按原顺序压缩到数组前端,剩余位置全部填充 0。


二、算法策略(双指针)

  • 使用快指针fast遍历数组,慢指针slow指向下一个非零元素应该存放的位置。

  • 初始化slow = 0

  • 遍历过程

    • nums[fast] != 0,则nums[fast]赋值给nums[slow],然后slow++

    • 无论当前元素是否为 0,fast都向后移动。

  • 遍历结束后slow之前的位置都已放置非零元素,slow到末尾的所有位置置为 0


三、正确性说明(简单版本)

快慢指针保证了所有非零元素按原顺序被依次“搬运”到数组前部,且不会丢失任何非零元素。因为slow始终指向下一个可放置非零元素的位置,而fast负责遍历所有元素,遇到非零就覆盖到slow处。最后将剩余位置置零,既保留了非零顺序,又确保了所有 0 都在末尾。该算法一次遍历即可完成,正确性由指针移动逻辑保证。


四、实现细节(边界防护)

  • 若数组长度n <= 1,直接返回(无需操作)。

  • 使用int slow = 0

  • for (int fast = 0; fast < n; ++fast)遍历:

    • nums[fast] != 0,则nums[slow++] = nums[fast]

  • 遍历结束后,从slown-1循环赋值0

  • 时间复杂度 O(n),空间复杂度 O(1),满足原地要求。


五、返回值(目标映射)

不需要返回值,原地修改数组,使所有 0 移动到末尾,非零元素相对顺序不变。

三.代码

class Solution { public: void moveZeroes(vector<int>& nums) { // 算法思路:双指针法 // left 指向当前可能存放非零元素的位置(也是等待被非零元素替换的位置) // right 从 left+1 开始,向后查找非零元素,一旦找到就与 left 交换, // 然后将 left 右移一位,继续处理。 // 这样就能保证所有非零元素按原顺序前移,所有零被移动到末尾。 int left = 0; int right = left + 1; int n = nums.size(); // 当右指针未越界时,持续扫描 while (right < n) { // 如果左指针指向0,说明此处需要被非零元素替换 if (nums[left] == 0) { // 如果右指针指向非零元素,则交换,将非零元素移到左指针位置 if (nums[right] != 0) { swap(nums[left], nums[right]); // 注意:交换后,left 位置变为非零,但 left 并未自增, // 下一次循环时会进入 else 分支将 left 和 right 都右移, // 相当于 left 指向了下一位,right 指向下下位,正确。 } else { // 如果右指针也指向0,则右指针继续右移,寻找非零元素 right++; } } else { // 如果左指针指向非零,说明当前位置已经正确,将两个指针同时右移 left++; right++; } } } };

四、流程图

🎯 闭幕

🎉 恭喜你完成了「移动零」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡深入思考

  • 本题要求原地移动零,且保持非零元素的相对顺序。常用的解法是双指针(快慢指针),slow指向已处理区域的末尾,fast用于遍历。请问slowfast各自的具体职责是什么?

  • 如果采用覆盖法(先移非零,再补零)与交换法(遇非零即与slow交换),两种方式的操作次数有何差异?哪种在时间复杂度相同的情况下更高效?

  • 若数组包含负数,题目只要求移动零,负数应视为非零元素保持顺序,算法逻辑是否需要改动

  • 如果要求将所有零移动到数组开头(而非末尾),你只需修改判断条件中的哪一处?请动手试一试。

  • 本题强制不复制数组,若允许复制,你会用怎样的额外空间方案实现?此时的时间复杂度是否变化?

📚延伸挑战

  • 若问题改为将指定值(不限于 0)全部移到末尾,且保持其他元素顺序,你的代码应做哪些通用化改造?

如果你觉得本文对你有所帮助,欢迎:

👍点赞 / 收藏
👤关注作者,获取更多题解
💬留言交流你的疑问或优化思路


深入思考答案

  • 双指针职责slow表示已排好的非零区域的下一个位置(即慢指针),fast用于遍历数组寻找非零元素。

  • 覆盖法 vs 交换法:两者时间复杂度均为 O(n),但覆盖法只需赋值(非零前移 + 末尾补零),交换法需三次赋值(交换),因此覆盖法常数更小,通常更快。

  • 负数处理:算法只判断!=0,负数被视为非零,无需改动。

  • 移动零到开头:将条件nums[fast] != 0改为nums[fast] == 0,并将非零值(如 1)补在末尾。

  • 允许复制:新建数组,先拷贝非零,再补零,最后复制回原数组,时间 O(n),空间 O(n)。

祝你在算法之路上越走越稳,早日攻克每一道难题!下次见 🚀✨

← 返回列表