移除元素【数组】

⭐前言⭐

※※※大家好!我是同学〖森〗,一名计算机爱好者,今天让我们进入练习模式。若有错误,请多多指教。更多有趣的代码请移步Gitee
👍 点赞 ⭐ 收藏 📝留言 都是我创作的最大的动力!

题目

27. 移除元素

题目:

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

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

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


示例1

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

示例2

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

提示

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

思路

题目分析:

  • 去除掉数组 nums 中 值为 val 的元素,返回新数组的长度;
  • 不能使用额外数组空间,因为要删除元素,新数组的长度 一定小于等于 原来数组元素,我们可以在原数组上进行操作
  • 元素的顺序可变,这个是我们进行优化的关键
  • 由提示可知,根据相关数据的范围,本题可以直接使用int类型

由于数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖;

解法一:使用暴力破解,用两层循环,第一层循环用于 寻找数组中等于 val 的元素,第二层循环负责如果第一层循环找到等于 val 的元素,就将该元素后面的元素向前一步,覆盖掉该元素;

解法二: 使用快慢指针,定义 fast = slow = 0; fast 指针用来查询当前数组的元素的值 是否为 val 如果 等于 val 就 跳过该元素,如果不等就将该元素赋值给 slow ,有此可知 slow 前面的元素都是值 不为 val 的元素,返回slow 就是新数组的长度;

解法三: 使用相向双指针,题解二中如果数组中没有val 元素那么fast 和slow 都会遍历一遍数组,会遍历两遍数组,但由于题目说明的是元素的顺序可变,我们可以用双指针分别从前向后(left),和从后向前遍历(right) 当 right 和 left 相遇的时候说明遍历了数组,我们要做的就是将left 遍历到 值等于 val 的元素和right 遍历的不等于 val 的值进行互换,这样就不用一个一个地移动元素,但是缺点却是改变了元素的原有顺序位置;

解法一:暴力破解

使用双层 for 循环

  • 第一层 for 循环 遍历数组元素

i = 0; i < len; i ++

  • 第二层for循环 更新数组元素

    如果 nums[i] == val
    for(int j = i + 1; j < len ; j++) nums[j - 1] = nums[j];
    将i后面值向前一位进行覆盖;

程序源码

// 时间复杂度: O(n^2)
// 空间复杂度: O(1)
public class Solution {
    public static int removeElement(int[] nums, int val) {
        // 双重循环
        int len = nums.length;
        for (int i = 0; i < len; i++) { // 第一层循环, 查找值为 val 的元素
            if (nums[i] == val) {   // 找到值为 val 的元素, 将i后面的元素集体向前移动一位
                for (int j = i + 1; j < len; j++) {
                    nums[j - 1] = nums[j]; // 注意 j的起始值 和 结束条件
                }
                i--;  // 因为 i 以后的元素都集体向前移动一位, 此时位于下标 i 的元素, 其实是下标为i + 1的元素,需要重新遍历;
                len--;  // 覆盖一个元素, 数组的长度 - 1;
            }
        }
        return len; // 返回数组的长度;
    }
}

例:nums = [0,1,2,2,3,0,4,2], val = 2 移除 nums 中的 2

使用暴力破解删除过程为:

初始化:i = 0

在这里插入图片描述


当 i = 0, 1时 nums[ i ] != 2 不执行if 语句 i = 2 时,进入if语句 j = i + 1;

在这里插入图片描述


执行 第二层循环 更新数组元素 : 将 i 后面的所有元素集体想前移动一位
此时灰色的框的元素 2 仍然存在数组中,我们需要将数组的长度由 8 改为 7 将最后一个元素 删除掉
注意 for循环的结束条件 是 i < len 而不是 i < nums.length

在这里插入图片描述

i --; len--;
这里需要重新遍历 下标为 2 的元素 需要 i-- 向前一步,同时 新数组的长度变成了7 循环到 下标为 6的时候就结束了,
注意要在循环的过程中更新结束的len值

在这里插入图片描述

i++ i = 2; 重复上述过程

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

接下来过程和 i = 0 ,i 时一样 i 一直++到 i == 5

在这里插入图片描述

此时 j = 6 = len 不进入第二层for循环 执行 i--len-- 操作

在这里插入图片描述

此时 i ++ 后 i = 5 i = len 不满足进入第一层for循环条件,跳出循环 新数组长度为 5 元素分别是:0 1 3 0 4 符合预期

使用暴力破解 的时间复杂度 为:O(n^2) 时间复杂度较高,我们能不能进一步优化下;

解法二:快慢指针(双指针)

快慢指针: 通过一个快指针和一个慢指针在一个for循环下完成两个for循环的工作
定义快慢指针:

  • 快指针: 寻找新数组的元素,即元素的值不等于 val 的元素,
  • 慢指针: 指向更新 新数组下标的位置,即新数组的最后一个元素的下一个的位置,和新数组的长度相等

思路: 快指针查询不等于val的元素,将这些数组赋值给慢指针所在的位置,如果查询到等于 val 的元素,快指针就跳过该元素;

完整代码:

// 时间复杂度: O(n)
// 空间复杂度: O(1)
public class demo2 {
    public static int removeElement(int[] nums, int val) {
        // 这种实现方法没有改变元素的相对位置
        int slow = 0;
        for (int fast = 0; fast < nums.length; fast++) {
            if (nums[fast] != val) {
                nums[slow] = nums[fast];
                slow++;
                // 这里可以化简为 nums[slow++] = nums[fast]; ++在slow的后面, 表示先用后加
            }
        }
        return slow;
    }
}

例:nums = [0,1,2,2,3,0,4,2], val = 2 移除 nums 中的 2

图解:
初始化:
在这里插入图片描述

nums[fast] != val 0 != 2 nums[slow] = nums[fast];

在这里插入图片描述

fast = 1; nums[1] != val

在这里插入图片描述

fast = 2 nums[2] == val

在这里插入图片描述

fast == 3 nums[3] == val

在这里插入图片描述

fast == 4 nums[4] != 2

在这里插入图片描述

fast = 5 nums[5] != val

在这里插入图片描述

fast = 6 nums[6] != val

在这里插入图片描述

fast = 8 nums[7] == val

在这里插入图片描述

fast = 8 == nums.length 跳出循环
此时slow 所值的下标位置,正好等于新数组(橙色部分)的长度;
将slow返回;

这次解法比上一次解法时间复杂度直接从O(n^2) 降低到 O(n)但这种算法在最坏情况下(数组没有元素等于val)需要左右指针各遍历一次数组,遍历了两次数组,而且还进行了无效复制 nums[slow] = nums[fast](fast == slow时)

解法三:双指针优化

相向双指针:通过左右两个指针,向中间遍历实现只遍历一次完成删除的工作

  • 前提: 元素的顺序可以改变
  • 思路:如果要移除的元素在数组开头,我们可以直接从后面不排除的元素移动到开头这个元素的位置
    例:【3 4 6 2 7 8】 val 3 我们直接可以用 8 来替代 3 得到 【8 4 6 2 7】同样满足题目
    这个优化在序列中val 元素的数量较少时效果较明显;

实现 :

  • 使用left = 0; right = nums.length; 向中间遍历,
  • 如果 left 指向的元素等于 val 就将 right 指向的元素 复制到left的位置,然后right–;
  • 如果 left 指向的元素 不等于 val, 就left ++
  • 会有一种情况是right 指向的元素也等于 val ,不过没有关系,因为上一步中 没有left++;还会检查新复制来的元素是不是为val;
  • 当left 和 right重合的时候就遍历完数组中所有元素,结束循环

完整代码

// 时间复杂度: O(n)
// 空间复杂度: O(1)
public class Solution {
    public static int removeElement(int[] nums, int val) {
        int left = 0;  // 指向数组第一个元素
        int right = nums.length - 1;  // 指向数组最后一个元素
        while(left <= right){
            if (nums[left] == val) {
                // 如果 left 所指向的元素等于 val 就将 right 指向的元素复制到 left 的位置
                nums[left] = nums[right--];
            } else {
                // 不等于 val, 就left++;
                left++;
            }
        }
        return left;
    }
}

例:nums = [0,1,2,2,3,0,4,2], val = 2 移除 nums 中的 2

初始化 left = 0; right = 7

在这里插入图片描述

left = 0 nums[0] != val

在这里插入图片描述

left = 1 nums[0] != val

在这里插入图片描述

left = 2 nums[0] == val
nums[left] = nums[right–]

在这里插入图片描述

left = 2 nums[0] == val
nums[left] = nums[right–]

在这里插入图片描述

left = 2 nums[2] == 4 != 2
left++

在这里插入图片描述

left = 3
nums[3] = 2 == val
nums[left] = nums[right–]

在这里插入图片描述

left = 3 nums[3] != val
left++;

在这里插入图片描述
left = 4 nums[4] != val
left++;
在这里插入图片描述

left > right
跳出循环, 返回 left ,5

这种算法两个指针在最坏的情况下合起来只遍历了数组一次,避免了需要保存的元素的重复复制操作

但是这种算法重复第判断了好几次在同一个下标位置,对于right指向的元素 等于val 的情况还复制了过去,还存在着一定的问题,所以我们还可以进行优化

寻找left 指向的元素 等于 val 和 right 指向的元素 不等于 val,这种情况下才进行复制

完整代码

// 时间复杂度: O(n)
// 空间复杂度: O(1)
public class Solution {
    public static int removeElement(int[] nums, int val) {
        int left = 0;  // 指向数组第一个元素
        int right = nums.length - 1;  // 指向数组最后一个元素
        while (left <= right){
            while (left <= right && nums[left] != val){ // 寻找左边等于 val 的元素
                left++;
            }
            while (left <= right && nums[right] == val){ // 寻找右边不等于 val 的元素
                right--;
            }
            if (left < right) { // 将右边不等于val的元素覆盖掉左边等于val的元素

                nums[left++] = nums[right--];
            }
        }
        return left;  // left 制定指向最终数组末尾的下一个元素;
    }
}

例:nums = [0,1,2,2,3,0,4,2], val = 2 移除 nums 中的 2

图解 : 初始化
left = 0; right = 7

在这里插入图片描述

寻找左边等于 val 的元素 left = 2

在这里插入图片描述

寻找右边不等于 val 的元素 right = 6

在这里插入图片描述

left < right
nums[left++] = nums[right–]; 将右边不等于val的元素覆盖掉左边等于val的元素

在这里插入图片描述

left <= right 进入循环
寻找左边等于 val 的元素 left = 3
寻找右边不等于 val 的元素 right = 5
left < right
nums[left++] = nums[right–]; 将右边不等于val的元素覆盖掉左边等于val的元素

在这里插入图片描述

left == right 进入循环
寻找左边等于 val 的元素 left = 5
left > right跳出循环
left > right 不会进入循环 并且也不会 进行交换

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

同学〖森〗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值