[数据结构与算法] 数组-移除元素篇

 一.27. 移除元素 - 力扣(LeetCode)

题目:

给你一个数组 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。

 题目分析:

我在第一次看到这道题时,第一个想到的就是,遍历嘛,都遍历一次,然后把需要的元素(不等于val的)放入新数组中即可。

这种办法是最简单想到的,也是最容易理解和实现的,但是题目要求:不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。所以这种办法是不行的,因为我们创建了一个新的数组。

那就只能在这一个数组上做文章了,怎么做呢?题目要求是删除元素,我们知道从数组中删除元素并不是直接把要删除元素拿走就完事了。因为数组在内存中是一块连续的空间:

数组在内存中是这样连续的,中间不可断开

这就意味着我们只能通过覆盖来进行删除,因为只有这样,才能保证数组数据的连续性。

具体怎么覆盖呢?就是找到要删除元素的位置,然后把其后一个元素赋值给自身即可。这个过程要不断进行,直到数组结束。如图所示,假设要删除2

从2往后每一个元素都要前移覆盖前一个元素,达到删除2的效果

这就是本题的第一种解法,不太推荐,但必须要会。

注意以下几点:

1.覆盖元素操作时,不要写nums[j] = nums[j + 1];这样理论上可以完成,但是出现数组索引越界的可能性比较大,所以最好把j作为索引最大值,这不可能越界,因为j < nums.length;

2.每次移除后,此时i指向被删除位置,移除完毕后,并不知道移动到i位置的元素是否要删除,所以要回退i,不然就错过这个元素了

解法一:暴力删除

class Solution {
    public int removeElement(int[] nums, int val) {
        //1.暴力解法
        int size = nums.length;
        for(int i = 0; i < size; i++){
            //如果找到要删除的元素了,往后的元素都要前移
            if(nums[i] == val){
                for(int j = i + 1; j < size; j++){
                    nums[j - 1] = nums[j];
                }
                i--;
                size--;
            }
        }
        return size;
    }
}

 解法二:双指针(重要)

这道题的第二种办法就是用两个指针,一层循环,来解决上面两层循环干的事情。

这两个指针分别被称为快指针和慢指针,下面分别说说它们的作用:

快指针fast:用于记录新数组要存储的元素,也就是不等于val的元素,这个指针向后遍历整个数组,而且是不断遍历,一直往后走。

慢指针slow记录新数组存储元素的位置,可以理解为一个虚空数组的各个索引。这个指针之所以叫做慢指针,就是因为它不是一直往后走的,只是在满足一定条件下才会往后走,什么条件呢?就是当所指向的位置需要存储新元素了,他就会往后走。

接下来让我用图示来解释整个过程:

初始时,fast和slow都指向索引为0的位置,此时fast指向的元素值为1,和要删除的val = 2 不同,所以这个元素1就是新数组要存储的元素。存储到哪呢?就存储到slow指向的位置,slow当前指向的位置就是新数组的索引0(一个虚空的数组,和原数组共用一块空间),然后slow往后移动,fast随后也往后移动,就到了图二

图一:初始情况

接着,fast判断当前元素是不是要删的,结果发现就是要删除的val = 2,此时就不会把这个值赋值给slow维护的新数组,slow也就不会后移,而是在索引1处待命,但是fast就要继续向后寻找。

图二:准备删除

此时fast指向3,发现不是要删的,所以准备把这个值给到新数组,然后就赋值给slow维护的新数组,并且slow++,走向下一个索引位置待命,fast随后也++; 

图三

其实这三幅图完全就说明了两个指针的作用。总结一下,一个快指针一个慢指针,快指针就是个苦力,不断地后移寻找我们需要的元素;而慢指针只需要把这些需要的元素收集起来,并且维护好下一次要存放的位置即可。 

 这是双指针的典型应用,通过这题我理解了两个指针相互配合时,各自都要有明确的分工,每个指针干的事情都要非常清楚,才能解决问题。

class Solution {
    public int removeElement(int[] nums, int val) {
        //2.双指针法
        int slow = 0;//慢指针,用于记录新数组存储元素的位置
        //快指针,用于记录新数组要存储的元素
        //fast不断往后走
        for(int fast = 0; fast < nums.length; fast++){
            //如果不等于val,说明这个元素不该被删除,而是应该存在于新数组中
            //如果等于val,不会走if,此时fast指向的值就不会给slow指向的位置
            if(nums[fast] != val){
                nums[slow] = nums[fast];
                slow++;
            }
        }
        return slow;
    }
}

二.26. 删除有序数组中的重复项 - 力扣(LeetCode)

class Solution {
    public int removeDuplicates(int[] nums) {
        
        int slow = 0;
        //fast指针指向的值一旦改变,就要移动慢指针一位并且把慢指针指向的值改为快指针指向的值
        for(int fast = 1; fast < nums.length; fast++){
            if(nums[fast - 1] != nums[fast]){
                slow++;
                nums[slow] = nums[fast];
            }
        }
        return slow + 1;
    }
}

  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值