面试高频题 力扣 283.移动零 双指针技巧 原地修改 顺序保持 C++解题思路 每日一题

发布于:2025-08-01 ⋅ 阅读:(16) ⋅ 点赞:(0)

零、题目描述

题目链接:移动零

题目描述:
在这里插入图片描述

示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:
输入: nums = [0]
输出: [0]

提示:
1 <= nums.length <= 10^4
-2^31 <= nums[i] <= 2^31 - 1

一、为什么这道题值得你花几分钟看懂?

如果你正在打磨自己的算法基础,那「移动零」这道题你一定不能错过——它是 LeetCode 第 283 题,更是双指针技巧的经典入门题,几乎所有算法入门教程都会拿它举例。

从算法能力提升来讲,这道题能帮你深刻理解双指针在原地修改数组中的核心作用,掌握「快慢指针配合」的精髓。这种技巧不仅能解决数组中的元素移动问题,还能延伸到链表操作、滑动窗口等多种场景,是很多高效算法的基础。

甚至在实际开发中,当需要对数据进行过滤、整理,又希望节省空间时,双指针的思路能帮你写出更高效、更优雅的代码。

二、题目拆解:提取其中的关键点

先看原题:

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。必须在不复制数组的情况下原地对数组进行操作。

再结合所给的代码框架和提示:

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        
    }
};

核心要素提炼:

  1. 输入是一个整数类型的向量nums,需要对其进行原地修改。
  2. 操作目标是将数组中的所有0移到末尾,同时保证非零元素的相对顺序不变。
  3. 不能使用额外的数组来辅助,必须在原数组上进行操作。

关键点:

  1. 双指针应用:用两个指针分别追踪非零元素的位置和当前遍历的位置。
  2. 元素交换:将非零元素移动到前面合适的位置。
  3. 保持顺序:确保非零元素的相对位置和原来一致。
  4. 原地操作:不使用额外的数组空间,只在原数组上进行修改。

三、明确思路:双指针的巧妙配合

1. 最直观的想法:先移非零,再补零

拿到题的第一反应:把所有非零元素按顺序移到数组前面,然后把剩下的位置都填上0。比如对于数组[0,1,0,3,12],先把非零元素1312依次移到前面,得到[1,3,12,...],再把后面的位置补成0,结果就是[1,3,12,0,0]

这种思路的关键是如何高效地找到非零元素应该放置的位置,这就需要用到双指针了。

2. 双指针的分工

在这里,我们可以用两个指针:

  • cur指针:负责遍历整个数组,寻找非零元素,相当于“探索者”。
  • dest指针:负责记录下一个非零元素应该放置的位置,相当于“占位者”。

初始时,cur从数组开头开始遍历,dest指向-1(表示还没有确定非零元素的位置)。当cur遇到非零元素时,就把dest向前移动一位,然后交换curdest指向的元素。这样,dest始终指向已经处理好的非零元素的最后一个位置。

举个例子(示例1):

初始数组:[0,1,0,3,12],cur=0,dest=-1
cur=0,元素是0,不做操作,cur++
cur=1,元素是1(非零),dest++变为0,交换nums[0]和nums[1],数组变为[1,0,0,3,12],cur++
cur=2,元素是0,不做操作,cur++
cur=3,元素是3(非零),dest++变为1,交换nums[1]和nums[3],数组变为[1,3,0,0,12],cur++
cur=4,元素是12(非零),dest++变为2,交换nums[2]和nums[4],数组变为[1,3,12,0,0],cur++
遍历结束,得到结果

3. 为什么这样可行?

因为cur指针一直在dest指针前面或者和它同步,dest指针前面的元素都是已经处理好的非零元素,并且保持着原来的相对顺序。当cur遇到非零元素时,把它交换到dest+1的位置,就保证了非零元素按原来的顺序排列在前面,而后面剩下的位置自然都是0了。

这种方法只需要遍历一次数组,并且是原地操作,时间复杂度是O(n),空间复杂度是O(1),非常高效。

四、算法实现:双指针的代码演绎

这道题我用双指针的方法来实现,下面讲解具体的算法思路。

核心逻辑:
cur指针遍历数组,当遇到非零元素时,将dest指针向前移动一位,然后交换curdest指向的元素,这样就能把非零元素逐步移到前面,零自然就到后面去了。

步骤拆解:

  1. 初始化cur为0,dest为-1。
  2. cur遍历数组中的每个元素:
    • nums[cur]是非零元素:
      • dest向前移动一位(dest++)。
      • 交换nums[cur]nums[dest]
    • cur向前移动一位(cur++)。
  3. 遍历结束后,数组中的所有零都被移到了末尾,非零元素保持相对顺序不变。

双指针细节:

  1. cur指针:从0开始,依次遍历数组的每个元素,直到数组末尾。它的作用是发现非零元素。
  2. dest指针:初始值为-1,表示还没有非零元素被放置。每当cur找到一个非零元素,dest就向前移动一位,指向这个非零元素应该放置的位置。
  3. 交换操作:当cur找到非零元素时,交换nums[cur]nums[dest],这样非零元素就被放到了前面合适的位置。
  4. 遍历顺序cur始终从左到右遍历,保证了非零元素的相对顺序不变。

五、C++代码实现:一步步拆解

完整代码(附详细注释):

#include <vector>
using namespace std;

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size();
        // cur用于遍历数组,dest用于记录非零元素应放的位置
        for (int cur = 0, dest = -1; cur < nums.size(); cur++) {
            if (nums[cur] != 0) {  // 遇到非零元素
                dest++;  // dest移动到下一个位置
                swap(nums[dest], nums[cur]);  // 交换元素,将非零元素放到前面
            }
        }
    }
};

代码拆解

1. 变量初始化

int n = nums.size();  // 获取数组长度,虽然这里没有直接使用,但可以用于后续扩展
int cur = 0;  // 遍历指针,从数组开头开始
int dest = -1;  // 目标指针,初始为-1,表示还没有非零元素被放置

作用cur负责遍历整个数组,dest负责跟踪非零元素应该放置的位置,初始状态下还没有非零元素被处理,所以dest为-1。

2. 主循环逻辑

for (int cur = 0, dest = -1; cur < nums.size(); cur++) {
    // 循环条件:cur遍历完数组的所有元素
    if (nums[cur] != 0) {  // 当遇到非零元素时
        dest++;  // 目标位置向前移动一位
        swap(nums[dest], nums[cur]);  // 交换元素,将非零元素放到目标位置
    }
    // 如果是零元素,则不做处理,cur继续向前移动
}

核心设计

  • 遍历机制cur从0开始,每次循环后自增1,直到遍历完整个数组,确保每个元素都被检查。
  • 非零元素处理:当nums[cur]不是0时,说明找到了一个需要放到前面的元素。此时dest先自增1,指向当前应该放置非零元素的位置,然后交换nums[cur]nums[dest],这样非零元素就被放到了前面。
  • 零元素处理:当nums[cur]是0时,不做任何操作,cur继续向前移动,零元素就被留在了原地,最终会被后面的非零元素交换到后面去。

示例走读(示例1:nums = [0,1,0,3,12])

步骤 cur dest nums[cur] 操作 数组状态
初始 0 -1 0 不操作,cur++ [0,1,0,3,12]
1 1 -1 1 dest++变为0,交换nums[1]和nums[0],cur++ [1,0,0,3,12]
2 2 0 0 不操作,cur++ [1,0,0,3,12]
3 3 0 3 dest++变为1,交换nums[3]和nums[1],cur++ [1,3,0,0,12]
4 4 1 12 dest++变为2,交换nums[4]和nums[2],cur++ [1,3,12,0,0]
结束 5(退出循环) 2 - - [1,3,12,0,0]

通过这个过程可以清晰地看到,非零元素1、3、12依次被放到了数组前面,零元素被移到了后面,并且非零元素的相对顺序保持不变。

时间复杂度和空间复杂度

复杂度类型 具体值 说明
时间复杂度 O(n) n是数组的长度,cur指针遍历整个数组一次,每个元素最多被交换一次
空间复杂度 O(1) 只使用了两个额外的指针变量,没有使用额外的数组或其他数据结构

这种算法在时间和空间上都非常高效,符合题目的要求。

六、实现过程中的坑点总结

  1. dest指针的初始值
    错误写法

    for (int cur = 0, dest = 0; cur < nums.size(); cur++) {
        // ...
    }
    

    问题:如果dest初始化为0,当数组的第一个元素是非零元素时,会把它和自己交换,虽然结果正确,但做了无用功。更重要的是,如果数组全是零元素,可能会出现不必要的操作。
    正确做法:初始化为-1,让第一个非零元素能正确地放到索引0的位置。

  2. 忘记交换元素
    错误写法

    if (nums[cur] != 0) {
        dest++;
        nums[dest] = nums[cur];  // 直接赋值,没有处理原来的位置
    }
    

    问题:这样会导致原来dest位置的元素被覆盖,而cur位置的非零元素没有被清除,可能会留下重复的值。
    正确做法:使用swap函数交换两个位置的元素,确保非零元素移动的同时,零元素被换到后面。

  3. 遍历顺序错误
    错误写法

    for (int cur = nums.size() - 1; cur >= 0; cur--) {
        // ...
    }
    

    问题:从后往前遍历会打乱非零元素的相对顺序,无法保证移动后非零元素的顺序和原来一致。
    正确做法cur指针必须从左到右遍历数组,才能保证非零元素的相对顺序不变。

  4. 处理空数组或只有一个元素的情况
    虽然代码中没有专门处理,但当前的代码逻辑对这些情况是兼容的:

    • 如果数组为空,循环不会执行,直接返回。
    • 如果数组只有一个元素,无论是0还是非零,循环执行一次后都会得到正确的结果。
  5. 不必要的交换
    curdest相等时(比如数组开头都是非零元素),交换操作是不必要的。可以添加一个判断来优化:

    if (nums[cur] != 0) {
        dest++;
        if (cur != dest) {  // 只有当位置不同时才交换
            swap(nums[dest], nums[cur]);
        }
    }
    

    这样可以减少一些无用的交换操作,提高代码效率。

七、举一反三

学会这道题的双指针技巧,你能解决很多类似的数组操作问题:

  • LeetCode 27. 移除元素:给定一个值,移除数组中所有等于这个值的元素,思路是用双指针将不等于该值的元素移到前面。
  • LeetCode 26. 移除重复元素:删除有序数组中的重复项,让每个元素只出现一次,用双指针可以高效地实现。
  • LeetCode 80. 删除有序数组中的重复项 II:允许元素最多出现两次,双指针的思路可以扩展应用。

明天我们一起讨论LeetCode 1089. 复写零这道题有兴趣的朋友可以提前思考下

双指针技巧在数组和链表操作中非常常见,掌握它能让你在处理元素移动、删除、查找等问题时更加得心应手。

最后,欢迎大家在评论区分享自己的代码和思路,一起交流学习~ 如果你有更巧妙的方法,也请不吝赐教,我会认真学习并回复的!
在这里插入图片描述

这是今天的封面原图:
在这里插入图片描述