力扣hot100之283:移动零
力扣hot100之283:移动零

LeetCode 283. 移动零

题目描述

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

注意:

  • 必须在原数组上操作,不能拷贝额外数组

  • 尽量减少操作次数


示例分析

示例 1

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

解释:

  • 所有非零元素按原来的顺序保留下来

  • 所有 0 被统一移动到数组末尾


示例 2

输入: nums = [0]
输出: [0]

只有一个元素,而且本身就是 0,所以结果不变。


解题思路

这道题的核心要求有两个:

  1. 把所有 0 移动到末尾

  2. 非零元素的相对顺序不能变

很多同学第一次看到这题,可能会想到“遇到 0 就和后面的非零元素交换”。 这种做法虽然能做出来,但写起来麻烦,而且交换次数也可能比较多。

你给出的这份代码思路非常清晰,本质上是一个双指针覆盖写入的方法:

  1. 先把所有非零元素按顺序搬到数组前面

  2. 再把后面剩余的位置统一补成 0

这样既满足“原地修改”,也保证了非零元素顺序不变。


代码

class Solution {
    public void moveZeroes(int[] nums) {
        if(nums==null) {
            return;
        }
        int j = 0;
        for(int i=0;i<nums.length;++i) {
            if(nums[i]!=0) {
                nums[j++] = nums[i];
            }
        }
        for(int i=j;i<nums.length;++i) {
            nums[i] = 0;
        }
    }
}

双指针分别表示什么

这份代码里有两个关键下标:

  • i:负责遍历整个数组,寻找每一个非零元素

  • j:负责指向下一个应该放置非零元素的位置

你可以把它理解成:

  • i 是“读指针”

  • j 是“写指针”

i 从左到右扫描数组。 每当发现一个非零元素,就把它写到 j 的位置,然后 j 往后移动一格。

这样第一轮结束后,数组前面就会按顺序放好所有非零元素。


为什么这样能保持相对顺序

这是这道题最关键的一点。

因为我们是按照原数组从左到右的顺序遍历的:

for(int i = 0; i < nums.length; ++i)

只要遇到非零元素,就立刻按当前顺序写到前面去。

所以写入前面的非零元素顺序,和它们在原数组中出现的顺序完全一致。

例如:

[0,1,0,3,12]

扫描顺序是:

  • 先遇到 1

  • 再遇到 3

  • 最后遇到 12

所以前面被写成的结果一定是:

[1,3,12,...]

顺序不会乱。


第一轮循环在做什么

代码

int j = 0;
for(int i=0;i<nums.length;++i) {
    if(nums[i]!=0) {
        nums[j++] = nums[i];
    }
}

含义

这段代码的目标是:

把所有非零元素依次压缩到数组前面。


举个例子

假设:

nums = [0,1,0,3,12]

初始:

j = 0

i = 0

nums[0] = 0

跳过。


i = 1

nums[1] = 1

是非零元素,写到 nums[j]

nums[0] = 1
j = 1

数组变成:

[1,1,0,3,12]

这里虽然看起来中间有些值被覆盖得有点“乱”,但没关系,因为我们后面还会统一补零。


i = 2

nums[2] = 0

跳过。


i = 3

nums[3] = 3

写到 nums[1]

nums[1] = 3
j = 2

数组变成:

[1,3,0,3,12]

i = 4

nums[4] = 12

写到 nums[2]

nums[2] = 12
j = 3

数组变成:

[1,3,12,3,12]

第一轮结束后,数组前 j 个位置已经是所有非零元素:

[1,3,12, ... ]

这时候 j = 3,表示已经放好了 3 个非零元素。


第二轮循环在做什么

代码

for(int i=j;i<nums.length;++i) {
    nums[i] = 0;
}

第一轮只是把非零元素移到了前面,但数组后半部分还残留着旧值。 所以第二轮的任务就是:

把剩余位置全部填成 0

继续上面的例子:

第一轮后数组是:

[1,3,12,3,12]

现在从 j = 3 开始,把后面全部设为 0

  • nums[3] = 0

  • nums[4] = 0

最终得到:

[1,3,12,0,0]

这就是正确答案。


为什么这题算原地修改

题目要求不能额外开一个新数组。

你这份代码里只使用了:

  • 一个读指针 i

  • 一个写指针 j

并没有申请额外数组,所以满足题目的“原地操作”要求。


为什么不需要交换

很多人第一次做这题会写成“遇到非零元素就和前面的零交换”。

但你这份代码其实更简洁,因为它不是交换,而是“覆盖写入”。

这样做有两个好处:

  1. 逻辑更清楚

  2. 操作次数通常更少

因为每个非零元素最多只写一次,最后再统一补零,不需要来回交换。


代码逐段解析

1. 判空

if(nums==null) {
    return;
}

如果数组为 null,直接返回,防止空指针异常。


2. 初始化写指针

int j = 0;

j 表示下一个非零元素应该写入的位置。


3. 第一轮:收集所有非零元素

for(int i=0;i<nums.length;++i) {
    if(nums[i]!=0) {
        nums[j++] = nums[i];
    }
}

这一轮结束后:

  • nums[0...j-1] 都是原数组中的非零元素

  • 且顺序与原数组一致


4. 第二轮:补零

for(int i=j;i<nums.length;++i) {
    nums[i] = 0;
}

把剩下的所有位置填成 0


再看一个例子

假设:

nums = [4,0,5,0,0,3,0,1]

第一轮扫描后,前面会变成:

[4,5,3,1,...]

此时 j = 4

第二轮把后面全部补零:

[4,5,3,1,0,0,0,0]

这就完成了“非零元素前移,零移到末尾”。


复杂度分析

设数组长度为 n

时间复杂度

O(n)

虽然有两轮循环,但总的仍然是线性复杂度。

  • 第一轮遍历一次数组

  • 第二轮最多再遍历一次数组

所以时间复杂度是 O(n)


空间复杂度

O(1)

只使用了常数个额外变量,没有使用额外数组。


和交换写法对比

这道题还有一种常见写法,也是双指针:

  • j 指向当前应该放非零元素的位置

  • nums[i] != 0 时,交换 nums[i]nums[j]

例如:

if (nums[i] != 0) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
    j++;
}

交换写法的优点

  • 一边遍历一边整理结果

  • 最终不需要再单独补零

当前覆盖写法的优点

  • 更直观

  • 代码更简洁

  • 更容易理解“先压缩非零元素,再统一补零”的思路

所以你这份代码非常适合作为这题的基础标准解法。


总结

这道题的核心可以概括成两步:

  1. 用一个写指针 j,把所有非零元素按顺序放到数组前面

  2. 再把剩余位置全部补成 0

也就是说:

前面放非零,后面补零。

你可以把这题记成一句话:

双指针,一个负责扫描,一个负责写入,把非零元素稳定前移,最后统一补零。

这是一道非常经典的数组双指针题,思路简单但很有代表性,值得牢牢记住。


如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇