📌 题目链接:leetcode.cn/problems/mo…
🔍 难度:简单 | 🏷️ 标签:数组、双指针、原地操作
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)
🧩 题目分析
给定一个整数数组
nums,要求将所有0移动到数组末尾,同时保持非零元素的相对顺序不变。
⚠️ 关键限制:原地操作,不能使用额外数组或复制结构。
📌 示例说明
输入: nums = [0,1,0,3,12]
输出: nums = [1,3,12,0,0]
- 所有非零元素
1, 3, 12保持原有顺序。 - 所有
0被移动到末尾。 - 数组被就地修改,没有新建空间。
🧠 核心算法:双指针法(Two Pointers)🎯
🔄 双指针技术是解决“数组重构”、“去重”、“归并”等问题的利器。
在本题中,我们采用 “快慢指针” 模型,实现高效原地操作。
🔍 为什么用双指针?
- 避免重复遍历:一次遍历完成非零元素前移。
- 原地修改:不依赖额外空间,符合题目要求。
- 稳定排序性质:保持非零元素的相对顺序(即稳定性)。
💡 小知识:这种模式也叫 “压缩非零元素” 或 “滑动窗口前移”。
🛠️ 算法实现(双指针法1)
✅ 解题思路(分步解析)
-
初始化:
- 定义
index指针(或称slow),指向下一个非零元素应放置的位置。 - 初始化为
0,表示第一个非零元素应放在索引0处。
- 定义
-
第一次遍历(快指针):
- 使用
i从头遍历数组。 - 当
nums[i] != 0时,将其赋值给nums[index],然后index++。 - 这样做的效果是:把所有非零元素依次“挤”到前面。
- 使用
-
第二次遍历(补零):
- 从
index开始到数组末尾,全部设为0。 - 因为前面已经处理完所有非零元素,剩下的位置自然就是
0的位置。
- 从
-
结束:
- 数组已满足条件:非零元素在前,
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:为什么不直接用 remove 或 push_back?
A:因为题目要求 原地操作,不能使用额外空间。
remove通常会返回新数组,push_back会改变容器大小,都不符合要求。
Q2:你能写出时间复杂度更低的解法吗?
A:不能。因为必须查看每个元素至少一次,所以 O(n) 是最优下限。
Q3:这个算法是否稳定?为什么?
A:是稳定的。因为我们只把非零元素按顺序“前移”,从未打乱它们的原始相对位置。
Q4:你能举出类似的双指针题目吗?
A:当然!例如:
💡 总结:这些题目都可以用 “快慢指针” 模板解决,核心是 “维护一个有效区域”。
🎯 核心思想总结(面试金句)
🧠 “双指针的本质是:用两个指针分别代表‘已处理’和‘待处理’区域,逐步推进边界。”
✅ 在本题中:
index指针:代表“非零元素已填满的最后一个位置”。i指针:代表“正在检查的当前元素”。- 通过不断比较和赋值,实现“非零元素前移”。
🔁 这种思想可以推广到:
- 数组去重
- 删除指定值
- 合并有序数组
- 滑动窗口
📣 下一期预告:LeetCode 热题 100 第5题 —— 盛最多水的容器(中等)
🔹 题目:给定
n个非负整数表示柱子的高度,找出由这些柱子构成的容器能容纳最多的水。🔹 核心思路:使用双指针从两端向中间收缩,每次选择较矮的一侧移动,以保证最大可能面积。
🔹 考点:双指针、贪心思想、面积最大化。
🔹 难度:中等,但逻辑清晰,是双指针的经典进阶题。
💡 提示:不要暴力枚举所有组合!双指针可以做到 O(n) 时间!
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
✨ 坚持每日一题,算法不再难! ✨