移除元素【数组】

news/2025/2/19 8:42:42/

⭐前言⭐

※※※大家好!我是同学〖森〗,一名计算机爱好者,今天让我们进入练习模式。若有错误,请多多指教。更多有趣的代码请移步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 不会进入循环 并且也不会 进行交换

在这里插入图片描述


http://www.ppmy.cn/news/50609.html

相关文章

【Java基础】Java中List集合的常用方法

在Java编程中&#xff0c;List集合是最常用的一种数据结构之一。它具有动态扩容、元素添加、删除和查询等基础操作&#xff0c;可以存储各种类型的对象&#xff0c;并且支持泛型。在本文中&#xff0c;我将介绍Java List集合的常用方法&#xff0c;并通过实例演示这些方法的使用…

Android进阶宝典—App响应时间优化

响应时间&#xff0c;它是用来衡量系统运行效率的一个重要指标。评价一个应用的响应时间&#xff0c;可以从用户感知和系统性能这两个角度来考量。 响应时间的长短&#xff0c;可能影响用户对某个功能、某个应用、乃至某个系统的使用。毕竟如果有选择&#xff0c;没有哪个人会愿…

C/C++设计模式之道:选择与权衡

C/C设计模式之道&#xff1a;选择与权衡 第1章&#xff1a;引言&#xff08;Introduction&#xff09;设计模式的概念与应用C中的设计模式及其优势 第2章&#xff1a;需求分析与场景划分&#xff08;Requirement Analysis and Scenarios&#xff09;项目需求分析方法场景划分与…

0201概述-网关Gateway-微服务架构

文章目录 1 前言2 项目引入3 术语4 工作原理5 配置示例5.1 简洁配置5.2 展开配置 6 Predicate7 GatewayFilter7.1 StripPrefix GatewayFilter7.2 RequestRateLimiter GatewayFilter① pom 依赖② 配置按照请求IP 的限流 6 Global Filters7 网关超时配置7.① 配置全局路由超时时…

python+opencv图片旋转函数-保持图像不被裁剪,且去除黑边

# 图片旋转函数-保持图像不被裁剪---顺时针 def ImageRotate(img, angle): # img:输入图片&#xff1b;newIm&#xff1a;输出图片&#xff1b;angle&#xff1a;旋转角度()height, width img.shape[:2] # 输入(H,W,C)&#xff0c;取 H&#xff0c;W 的值center (width //…

华为认证实验篇-ENSP的安装(附下载地址)

ENSP&#xff08;Enterprise Network Simulation Platform&#xff09;是华为公司开发的一款网络仿真软件&#xff0c;它可以帮助网络工程师进行网络拓扑设计、网络配置、网络测试等工作。本篇文章将介绍如何在Windows操作系统上安装ENSP。后续会在专栏陆续更新ENSP的实验&…

GitLab配置ssh key

git作为代码版本控制工具&#xff0c;在clone代码的时候选择ssh协议来拉取代码。本文讲解如何在Mac上生成ssh key&#xff0c;然后配置在gitlab里&#xff0c;最后使用ssh协议进行提交和拉取git远程仓库的代码。 本地只有一个ssh key 1、打开本地git bash,使用如下命令生成ss…

java中操作字符串都有哪些类?它们之间有什么区别?

Java中常用的字符串操作类有&#xff1a; 1.String类 String类是Java中最常用的字符串类&#xff0c;它是不可变的字符串&#xff0c;即创建后不能被修改。 2.StringBuilder类 StringBuilder类也是一个字符串操作类&#xff0c;但它是可变的&#xff0c;即可以修改已经创建的字…