【LeetCode Hot100 刷题日记(4/100)】283. 移动零——双指针、数组、原地操作💡

144 阅读7分钟

📌 题目链接:leetcode.cn/problems/mo…

🔍 难度:简单 | 🏷️ 标签:数组、双指针、原地操作

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)


🧩 题目分析

给定一个整数数组 nums,要求将所有 0 移动到数组末尾,同时保持非零元素的相对顺序不变
⚠️ 关键限制:原地操作,不能使用额外数组或复制结构。

image.png

📌 示例说明

输入: nums = [0,1,0,3,12]
输出: nums = [1,3,12,0,0]
  • 所有非零元素 1, 3, 12 保持原有顺序。
  • 所有 0 被移动到末尾。
  • 数组被就地修改,没有新建空间。

🧠 核心算法:双指针法(Two Pointers)🎯

🔄 双指针技术是解决“数组重构”、“去重”、“归并”等问题的利器。
在本题中,我们采用 “快慢指针” 模型,实现高效原地操作。

🔍 为什么用双指针?

  • 避免重复遍历:一次遍历完成非零元素前移。
  • 原地修改:不依赖额外空间,符合题目要求。
  • 稳定排序性质:保持非零元素的相对顺序(即稳定性)。

💡 小知识:这种模式也叫 “压缩非零元素”“滑动窗口前移”


🛠️ 算法实现(双指针法1)

✅ 解题思路(分步解析)

  1. 初始化

    • 定义 index 指针(或称 slow),指向下一个非零元素应放置的位置。
    • 初始化为 0,表示第一个非零元素应放在索引 0 处。
  2. 第一次遍历(快指针)

    • 使用 i 从头遍历数组。
    • nums[i] != 0 时,将其赋值给 nums[index],然后 index++
    • 这样做的效果是:把所有非零元素依次“挤”到前面
  3. 第二次遍历(补零)

    • index 开始到数组末尾,全部设为 0
    • 因为前面已经处理完所有非零元素,剩下的位置自然就是 0 的位置。
  4. 结束

    • 数组已满足条件:非零元素在前,0 在后,且顺序未变。

📊 算法分析(面试必备)

项目分析
🕒 时间复杂度O(n),只遍历数组两次(实际可合并成一次)
💾 空间复杂度O(1),仅使用常量级额外变量
🧱 原地操作✅ 支持,无需额外数组
🔁 稳定性✅ 保持非零元素相对顺序(关键点!)
🧪 边界情况支持全零、无零、全非零等极端情况
🧩 适用场景所有“保留部分元素并移动其他元素”的问题

面试加分点

  • 能说出“双指针”是通用模板,可用于类似问题(如删除指定元素、去重等)。
  • 强调“原地操作”的重要性——这是大厂对性能敏感岗位的核心考察点。

💻 完整代码(C++ 实现)

//Leetcode283.移动零
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 双指针
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size();
        int index = 0;  // 记录下一个非零元素应该放置的位置

        // 第一次遍历:将非零元素前移
        for (int i = 0; i < n; i++) {
            if (nums[i]) {  // 如果当前元素不为0
                nums[index++] = nums[i];  // 将非零元素移到index位置,然后index前进
            }
        }

        // 第二次遍历:将剩余位置填充为0
        while (index < nums.size()) {
            nums[index++] = 0;  // 从index开始到末尾全部设为0
        }
    }
};

//class Solution {
//public:
//    void moveZeroes(vector<int>& nums) {
//        int slow = 0;  // 慢指针:指向下一个非零元素应该放置的位置
//
//        // 第一次遍历:将非零元素前移
//        for (int fast = 0; fast < nums.size(); fast++) {
//            if (nums[fast] != 0) {
//                nums[slow] = nums[fast];  // 将非零元素移到前面
//                slow++;                   // 慢指针前进
//            }
//        }
//
//        // 第二次遍历:将剩余位置填充为0
//        for (int i = slow; i < nums.size(); i++) {
//            nums[i] = 0;  // 将末尾位置设为0
//        }
//    }
//};

int main() {
    Solution solution;

    // 测试用例1
    vector<int> nums1 = { 0, 1, 0, 3, 12 };
    cout << "输入: ";
    for (int num : nums1) cout << num << " ";
    solution.moveZeroes(nums1);
    cout << "\n输出: ";
    for (int num : nums1) cout << num << " ";
    cout << " (期望: 1 3 12 0 0)" << endl;

    // 测试用例2
    vector<int> nums2 = { 0 };
    cout << "输入: ";
    for (int num : nums2) cout << num << " ";
    solution.moveZeroes(nums2);
    cout << "\n输出: ";
    for (int num : nums2) cout << num << " ";
    cout << " (期望: 0)" << endl;

    // 测试用例3
    vector<int> nums3 = { 1, 2, 3, 4, 5 };
    cout << "输入: ";
    for (int num : nums3) cout << num << " ";
    solution.moveZeroes(nums3);
    cout << "\n输出: ";
    for (int num : nums3) cout << num << " ";
    cout << " (期望: 1 2 3 4 5)" << endl;

    // 测试用例4
    vector<int> nums4 = { 0, 0, 0, 1, 2 };
    cout << "输入: ";
    for (int num : nums4) cout << num << " ";
    solution.moveZeroes(nums4);
    cout << "\n输出: ";
    for (int num : nums4) cout << num << " ";
    cout << " (期望: 1 2 0 0 0)" << endl;

    return 0;
}

🧪 测试用例分析(面试必考)

输入输出说明
[0,1,0,3,12][1,3,12,0,0]典型案例,验证顺序和零位
[0][0]单元素边界
[1,2,3,4,5][1,2,3,4,5]无零,应无变化
[0,0,0,1,2][1,2,0,0,0]多个零在前,测试补零逻辑

面试提示:一定要写测试用例!尤其是边界情况,能体现你的严谨性。


🌐 扩展思考:如何优化?

虽然当前解法已经是 O(n) 时间 + O(1) 空间,但我们还可以进一步优化:

✅ 优化版本:单次遍历 + 交换(推荐)

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int slow = 0;
        for (int fast = 0; fast < nums.size(); fast++) {
            if (nums[fast] != 0) {
                swap(nums[slow], nums[fast]);
                slow++;
            }
        }
    }
};

🎯 优势:

  • 只需一次遍历,更简洁。
  • 仍为 O(n) 时间,O(1) 空间。
  • 更接近“原地交换”的本质。

❗ 注意:此方法通过交换实现,不会影响非零元素顺序,因为每次只把非零元素“拉”到前面,而后面的元素会被后续覆盖或交换。


📚 面试常见提问 & 回答策略

Q1:为什么不直接用 removepush_back

A:因为题目要求 原地操作,不能使用额外空间。remove 通常会返回新数组,push_back 会改变容器大小,都不符合要求。

Q2:你能写出时间复杂度更低的解法吗?

A:不能。因为必须查看每个元素至少一次,所以 O(n) 是最优下限

Q3:这个算法是否稳定?为什么?

A:是稳定的。因为我们只把非零元素按顺序“前移”,从未打乱它们的原始相对位置。

Q4:你能举出类似的双指针题目吗?

A:当然!例如:

💡 总结:这些题目都可以用 “快慢指针” 模板解决,核心是 “维护一个有效区域”


🎯 核心思想总结(面试金句)

🧠 “双指针的本质是:用两个指针分别代表‘已处理’和‘待处理’区域,逐步推进边界。”

✅ 在本题中:

  • index 指针:代表“非零元素已填满的最后一个位置”。
  • i 指针:代表“正在检查的当前元素”。
  • 通过不断比较和赋值,实现“非零元素前移”。

🔁 这种思想可以推广到:

  • 数组去重
  • 删除指定值
  • 合并有序数组
  • 滑动窗口

📣 下一期预告:LeetCode 热题 100 第5题 —— 盛最多水的容器(中等)

🔹 题目:给定 n 个非负整数表示柱子的高度,找出由这些柱子构成的容器能容纳最多的水。

🔹 核心思路:使用双指针从两端向中间收缩,每次选择较矮的一侧移动,以保证最大可能面积。

🔹 考点:双指针、贪心思想、面积最大化。

🔹 难度:中等,但逻辑清晰,是双指针的经典进阶题。

💡 提示:不要暴力枚举所有组合!双指针可以做到 O(n) 时间!


🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!

坚持每日一题,算法不再难!