移除元素

力扣27题目链接(opens new window)

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

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

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

示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。

示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

你不需要考虑数组中超出新长度后面的元素

初见思路

输入是一个数组nums和删减目标值val

那么遍历数组我认为是需要的,必须找出待移除的值

然后是如何移除目标值,对于数组来说,里面的值是连续的,所以不能直接就"删除"某个值

实际上我们调用del或者什么方法去删除数组中的值都只是把待删除的值后后面的值覆盖掉了,并没有真正的"删除"数组在该位置的空间,也就是说数组原来的大小是5 ,删除之后其在内存中仍然是5,只是返回的size变成了4

基于此,我们还需要一个循环来覆盖待删除的值

所以,暴力解法需要两层for循环,以下是代码实现

Java版(超时)

class Solution {
    public int removeElement(int[] nums, int val) {
        int nums_len = nums.length;
        for(int i = 0; i < nums.length; i++){
            if(nums[i] == val){
                for(int j = i + 1; j < nums.length; j++){
                    nums[j - 1] = nums[j];
                }
                i -= 1;
                nums_len -= 1;
            }
        }
        return nums.length;

    }
}

这种方法在java版下是超时的,c++版勉强不超时

可见O(n*n)复杂度的代码在刷题时几乎不可用

常规思路

原地移除元素通常会使用双指针(也叫快慢指针)的方法解决

这里的"双指针"的意思得先说清楚

首先,这个“指针”并不是指C/C++中的那个对象,它只是用来代指数组下标的一个变量,因此在任何语言中你都可以实现双指针,这更多的是一种思想在应用双指针时,需要考虑好每个指针的意义

那么,双指针在这里的含义是什么?

这里我们需要一个快指针和一个慢指针

  • 快指针用来在数组中寻找新数组需要的值【其实就是遍历数组】

  • 慢指针则代表着新数组的下标

最开始,快指针和慢指针的位置是一样的(想象一下)

 ↓(快指针)
[1,2,3,4]
 ↑(慢指针)

假设现在需要删除的数是3

那么我们需要先遍历数组【注意:我们不是要找待删除的数,而是要找构成新数组的数】,逻辑如下

int slow = 0;
for(int fast = 0; fast < nums.length; i++){
	if(nums[fast] != 3){
		...//做处理
	}
}
return slow;

现在遍历开始

==============step1
 ↓(快指针),1不需要删
[1,2,3,4]
 ↑(慢指针)       新数组的值:[1]
==============step2
   ↓(快指针),2不需要删
[1,2,3,4]
   ↑(慢指针)     新数组的值:[1,2]
==============step3
     ↓(快指针),3需要删,慢指针不更新
[1,2,3,4]
   ↑(慢指针)     新数组的值:[1,2]
==============step4 
覆盖
     ↓(快指针),后面的值覆盖3,4不需要删
[1,2,4, ←4]
     ↑(慢指针)     新数组的值:[1,2,4]
     
       ↓(快指针),遍历结束
[1,2,4,4]
     ↑(慢指针)     新数组的值:[1,2,4]

以上就是过程,如果觉得抽象建议去搜gif来看

解题模板

代码就按照上面的思路来写就好了

Java版
class Solution {
    public int removeElement(int[] nums, int val) {
        int slow = 0;//定义慢指针
        int nums_len = nums.length;
        for(int fast = 0; fast < nums_len; fast++){
            if(nums[fast] != val){//遍历并寻找构成新数组可用的数(即除了3以外的),然后添加按新数组下标slow添加到新数组中
                nums[slow] = nums[fast];
                slow += 1;
            }

        }
        return slow;//此时的slow返回的正好是数组大小(不是从0开始的)
        
    }
}
Python版
class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        slow = 0 #慢指针
        fast = 0 #快指针
        nums_len = len(nums)
        while fast < nums_len: #这里不可以用枚举方法,所以不用for来遍历数组,用while代替
            if nums[fast] != val:
                nums[slow] = nums[fast]
                slow += 1
            fast += 1 
        return slow

拓展练习

LeetCode26删除有序数组中的重复项

这题中,快指针仍然是为新数组寻找元素,这些元素的条件是均不相同,判断的方式是在快指针遍历数组的过程中,比较数组相邻两个元素是否相同,不同的即符合要求

慢指针仍为新数组的下标,只有在添加符合要求的数的时候才移动

注意:

  • 不要拿慢指针指向的数与快指针指向的数做比较
  • 数组中第一个数不用判断,因此快慢指针都从1开始
  • 更完美的解法还可以判断一下输入数组的元素是否大于1个

Java版

class Solution {
    public int removeDuplicates(int[] nums) {
        int slow = 1;//第一个数不用判断,因此从1开始
        for(int fast = 1; fast < nums.length; fast++){
            //快指针遍历数组,若相邻两数不同,则将数添加到新数组(新数组下标仍是慢指针)
            if(nums[fast - 1] != nums[fast]){
                nums[slow] = nums[fast];//此时fast - 1在数值上和slow一致
                slow += 1;
            }
        }
        return slow;
    }
}

Python版

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow = 1
        fast = 1
        num_len = len(nums)
        while fast < num_len:
            if nums[fast - 1] != nums[fast]:
                nums[slow] = nums[fast]
                slow += 1
            fast += 1 
        return slow
LeetCode283移动零

这题也有坑,最开始我按照常规思路做:快指针遇到非0的元素,给通过慢指针加到新数组,然后当前快指针处元素赋值0

class Solution {
    public void moveZeroes(int[] nums) {
        int slow = 0;
        int nums_len = nums.length;
        if(nums_len > 1){
            for(int fast = 0; fast < nums_len; fast++){
                if(nums[fast] != 0){
                    nums[slow] = nums[fast];
                    nums[fast] = 0;
                    slow += 1;
                }
            }
        }
    }
}

这里问题:没有考虑最开始的值是否为0,这导致有部分初始值不为0的用例无法通过【例如[1,0]】

修改:

不在向新数组添加元素后就将fast处元素置0,而是等到fast将数组遍历一遍之后,从slow处开始,将往后的元素全部置0即可

Java版

class Solution {
    public void moveZeroes(int[] nums) {
        int slow = 0;
        int nums_len = nums.length;
        if(nums_len > 1){
            for(int fast = 0; fast < nums_len; fast++){
                if(nums[fast] != 0){
                    nums[slow] = nums[fast];
                    slow += 1;
                }
            }
            for(int i = slow; i< nums_len; i++){
                nums[i] = 0;
            }
        }
    }
}

Python版(超时)

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        slow = 0
        fast = 0
        num_len = len(nums)
        while fast < num_len:
            if nums[fast] != 0:
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        
        while slow < num_len:
            nums[slow] = 0