问题描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
- 更改
nums数组,使nums的前k个元素包含不等于val的元素 nums的其余元素和nums的大小并不重要- 返回
k
示例
示例 1:
1 | 输入:nums = [3,2,2,3], val = 3 |
示例 2:
1 | 输入:nums = [0,1,2,2,3,0,4,2], val = 2 |
约束条件
0 <= nums.length <= 1000 <= nums[i] <= 500 <= val <= 100
解题思路
这道题的核心是使用 双指针技巧 来原地修改数组。关键思想是:
核心思路:使用两个指针,一个慢指针 slowindex 指向下一个有效元素应该放置的位置,一个快指针 fastindex 遍历整个数组。
双指针详细解析
指针作用
- 快指针
fastindex:负责遍历整个数组,寻找需要保留的元素 - 慢指针
slowindex:负责记录下一个有效元素应该放置的位置
核心逻辑
- 快指针遍历:
fastindex从 0 开始,逐个检查数组中的每个元素 - 条件筛选:只有当
nums[fastindex] != val时,才说明这是一个需要保留的元素 - 元素复制:将有效元素复制到慢指针位置:
nums[slowindex] = nums[fastindex] - 指针移动:复制完成后,慢指针自动向前移动:
slowindex++ - 跳过无效元素:当
nums[fastindex] == val时,快指针继续前进,慢指针不动
关键优势
- 一次遍历:只需要遍历一次数组,时间复杂度 O(n)
- 原地修改:不需要额外空间,直接在原数组上操作
- 逻辑清晰:快指针负责”找”,慢指针负责”放”
算法步骤
- 初始化:设置
slowindex = 0,fastindex = 0 - 遍历数组:使用
fastindex指针遍历整个数组 - 条件判断:
- 如果
nums[fastindex] != val,说明这是一个需要保留的元素 - 将
nums[fastindex]复制到nums[slowindex]位置 - 然后
slowindex指针向前移动一位
- 如果
- 返回结果:
slowindex的值就是移除指定元素后数组的长度
详细示例演练
示例1:nums = [3,2,2,3], val = 3
1 | 初始状态: |
示例2:nums = [0,1,2,2,3,0,4,2], val = 2
1 | 初始状态: |
实现细节
关键点分析
- 原地修改:不需要额外的数组空间,直接在原数组上操作
- 双指针移动:慢指针只在找到有效元素时才移动
- 元素覆盖:有效元素会覆盖掉需要移除的元素位置
边界情况处理
- 空数组:
nums.length = 0,直接返回 0 - 所有元素都等于 val:
slow始终为 0,返回 0 - 没有元素等于 val:所有元素都被保留,
slow等于原数组长度
代码实现
Java 实现(双指针方法)
1 | class Solution { |
代码逐行解析
1 | int slowindex = 0; // 初始化慢指针,指向数组的第一个位置 |
1 | for(int fastindex = 0; fastindex < nums.length; fastindex++) { |
1 | if(nums[fastindex] != val) { |
1 | nums[slowindex++] = nums[fastindex]; |
1 | return slowindex; // 慢指针的值就是有效元素的个数 |
对比:暴力解法 vs 双指针解法
暴力解法(你的第一个代码)
1 | public int removeElement(int[] nums, int val) { |
暴力解法的问题:
- 时间复杂度:O(n²) - 每次移除元素都需要移动后面的所有元素
- 效率低下:对于每个需要移除的元素,都要执行 O(n) 的移动操作
- 代码复杂:需要处理数组长度变化和索引回退
双指针解法(优化后)
1 | public int removeElement(int[] nums, int val) { |
双指针解法的优势:
- 时间复杂度:O(n) - 只需要一次遍历
- 空间复杂度:O(1) - 只使用常数额外空间
- 代码简洁:逻辑清晰,易于理解和维护
方法比较
| 方面 | 双指针方法 | 暴力方法 |
|---|---|---|
| 时间复杂度 | O(n) | O(n²) |
| 空间复杂度 | O(1) | O(1) |
| 遍历次数 | 1次 | 最多n次(每个移除元素都要移动) |
| 代码复杂度 | 简单 | 复杂 |
| 执行效率 | 高效 | 低效 |
| 优点 | 一次遍历,逻辑清晰 | 思路直观 |
| 缺点 | 需要理解双指针概念 | 效率低,代码复杂 |
| 推荐度 | ★★★★★ | ★★☆☆☆ |
复杂度分析
时间复杂度
- O(n):只需要遍历一次数组,其中 n 是数组的长度
- 每个元素最多被访问一次,没有嵌套循环
空间复杂度
- O(1):只使用了常数额外空间(两个指针变量)
- 没有使用额外的数组或其他数据结构
双指针技巧深入理解
为什么叫”双指针”?
虽然这里使用的是两个整数变量 slowindex 和 fastindex,但它们本质上是指向数组位置的”指针”:
- 指针:指向数组中某个位置的索引
- 双指针:使用两个索引来协同工作,解决数组问题
双指针的核心思想
分工明确:
- 快指针:负责”探索”,遍历整个数组
- 慢指针:负责”收集”,记录有效元素的位置
同步移动:
- 快指针总是向前移动
- 慢指针只在找到有效元素时才移动
空间复用:
- 利用数组前面的空间存储有效元素
- 避免使用额外的数组空间
双指针的适用场景
双指针技巧特别适用于以下类型的数组问题:
- 原地删除:需要删除数组中的某些元素
- 去重:移除数组中的重复元素
- 分区:将数组按某种条件分成两部分
- 合并:合并两个有序数组
常见双指针模式
模式1:快慢指针(本题使用)
1 | int slow = 0; |
模式2:左右指针
1 | int left = 0, right = nums.length - 1; |
调试技巧
为了更好地理解双指针的工作过程,可以在代码中添加调试输出:
1 | public int removeElement(int[] nums, int val) { |
关键收获
重要概念
- 双指针技巧:在处理数组问题时,双指针是常用的优化技巧
- 原地修改:在不使用额外空间的情况下修改数组
- 快慢指针:慢指针记录有效位置,快指针遍历数组
- 空间复用:利用数组前面的空间存储有效元素
常见陷阱
- 忘记移动慢指针:只有在找到有效元素时才移动慢指针
- 边界条件:注意处理空数组和所有元素都需要移除的情况
- 理解题意:题目不要求保持原有顺序,只需要前 k 个元素有效
- 索引混淆:注意区分快指针和慢指针的作用
解题思路总结
- 识别问题类型:需要原地删除数组元素
- 选择算法:双指针技巧是最优解
- 实现步骤:
- 初始化两个指针
- 快指针遍历,慢指针收集
- 返回慢指针位置
相关问题
这道题是双指针技巧的经典应用,掌握了这个模式后,可以解决很多类似的数组操作问题。通过对比暴力解法和双指针解法,我们可以清楚地看到算法优化的重要性。