leetcode
leetcode 251 ~ 300
移动零

移动零

难度:

标签:

题目描述

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

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

 

示例 1:

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

示例 2:

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

 

提示:

  • 1 <= nums.length <= 104
  • -231 <= nums[i] <= 231 - 1

 

进阶:你能尽量减少完成的操作次数吗?

代码结果

运行时间: 36 ms, 内存: 17.0 MB


/* 
 * 思路: 
 * 使用 Java Stream API 可以简洁地解决此问题。我们可以先过滤出所有非零元素,
 * 然后补充 0 来填充数组剩余部分。这个方法有一个缺点,就是它会创建一个新的数组,
 * 不符合题目中要求的原地操作。为了解决这个问题,我们可以在完成新数组构建后,
 * 手动将数据拷贝回原数组。
 */
 
import java.util.stream.IntStream;
import java.util.Arrays;
 
public class Solution {
    public void moveZeroes(int[] nums) {
        int[] nonZeroElements = IntStream.of(nums)
                                          .filter(num -> num != 0)
                                          .toArray();
        int zeroCount = nums.length - nonZeroElements.length;
        System.arraycopy(nonZeroElements, 0, nums, 0, nonZeroElements.length);
        Arrays.fill(nums, nonZeroElements.length, nums.length, 0);
    }
}

解释

方法:

本题解采用双指针的方法。初始时,两个指针都指向数组的开始位置。遍历数组,每当遇到非零元素时,就将该元素移动到第一个指针的位置,并将第一个指针向后移动一位。这样可以保证第一个指针之前的所有元素都是非零的。遍历完成后,将第一个指针及其之后的所有位置都填充为0,从而实现将所有0移动到数组末尾的目标。

时间复杂度:

O(n)

空间复杂度:

O(1)

代码细节讲解

🦆
在双指针方法中,为什么选择将非零元素前移而不是直接交换位置?
在双指针方法中,选择将非零元素前移而不是直接交换位置的原因是为了保持非零元素的原始顺序。如果直接交换位置,那么非零元素的相对顺序可能会被打乱。此外,前移操作通常比交换操作更高效,因为交换涉及更多的写操作。
🦆
在填充0的过程中,是否存在一种方法可以在第一次遍历的同时完成,以减少总的操作次数?
在第一次遍历的同时填充0是可能的。在移动非零元素时,可以立即将遍历到的位置设置为0,然后当移动到下一个非零元素时,再将其赋给前面的0位置。这样,第一次遍历完成后,数组后端自然填充了需要的0,从而减少了操作次数。但这需要确保不会覆盖还未处理的非零元素,可能需要增加条件判断来确保正确性。
🦆
如何确保在移动非零元素时不会覆盖尚未处理的元素,特别是在原地修改数组的情况下?
为了确保在移动非零元素时不覆盖尚未处理的元素,可以使用一个辅助指针(p1)。这个指针仅在确定一个元素为非零并需要被移动时才前移。通过这种方式,我们只在发现非零元素时才将其移动到p1指向的位置,并立即将p1移动到下一个位置,这样可以确保不会误操作尚未遍历到的元素。
🦆
在给定数组全为0的极端情况下,该算法的性能如何?是否有必要进行优化?
在数组全为0的情况下,该算法的性能表现良好。第一次遍历中,指针p1不会移动,因为没有非零元素。第二次遍历仅仅是重新确认填充0,实际上是多余的。在这种特定情况下,算法的时间复杂度仍为O(n),无需额外优化。这是因为即使优化掉第二次遍历,整体性能提升不大。

相关问题

移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

 

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

 

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

 

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100