LeetCode 27 移除元素
LeetCode 27 - 移除元素(Remove Element)是一个简单但经典的双指针问题,主要考察数组操作的基本功。虽然问题容易,但掌握多种解法以及衍生的变体问题对解决更复杂的操作数组问题有帮助。
题目描述
- 输入:整数数组
nums
和目标值val
。 - 要求:原地移除数组中所有等于
val
的元素,并返回移除后数组的长度。 - 注意:
- 数组的元素可以被改变,但空间复杂度要求 O(1)。
- 函数返回不等于
val
的元素的长度,最终结果的前部分元素排列无所谓。
示例:
输入:nums = [3, 2, 2, 3], val = 3
输出:2 (移除后的数组变为 [2, 2])
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5 (移除后的数组变为 [0, 1, 3, 0, 4])
常用解法及模板
解法 1:双指针法(快慢指针)
- 核心思想:用两个指针
slow
和fast
,fast
遍历数组所有元素,slow
记录结果数组的索引。- 如果
nums[fast] != val
,将nums[fast]
写到nums[slow]
位置,slow
自增。 - 如果
nums[fast] == val
,跳过不用写入。
- 如果
- 遍历结束后,
nums[0..slow-1]
是保留的非val
元素。
模板代码
class Solution {
public 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++; // 慢指针前进
}
}
return slow; // 最终的数组长度
}
}
复杂度分析
- 时间复杂度: O(n),遍历数组一次。
- 空间复杂度: O(1),原地完成。
解法 2:双指针法(交换法)
- 适用场景:
- 如果题目不要求保留数组的顺序,可以使用此解法。
- 每次找到
val
时,将其与数组最后一个元素交换,从而用较少的操作移除目标值。
- 核心思想:
- 使用双指针
left
(从头开始)和right
(从尾开始)。 - 如果
nums[left] == val
,将其与nums[right]
交换,并递减right
。 - 如果
nums[left] != val
,继续前进。 - 遍历结束时,
left
即为最终的结果长度。
- 使用双指针
模板代码
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0, right = nums.length - 1;
while (left <= right) {
if (nums[left] == val) {
nums[left] = nums[right];
right--;
} else {
left++;
}
}
return left; // 最终长度
}
}
复杂度分析
- 时间复杂度: O(n),每个元素最多遍历一次。
- 空间复杂度: O(1),原地操作。
解法 3:计数后重写
- 核心思想:先统计不等于目标值的所有索引,再将这些索引的值逐个复制回数组。
- 步骤:
- 遍历数组,统计有多少个非
val
的元素。 - 通过线性遍历将非
val
的值重写到数组的前段。
- 遍历数组,统计有多少个非
- 局限性:操作次数比双指针多,空间申请多一小部分,仍可 AC。
模板代码
class Solution {
public int removeElement(int[] nums, int val) {
int index = 0;
// 第一遍数非目标值
for (int i = 0; i < nums.length; i++) {
if (nums[i] != val) {
nums[index++] = nums[i];
}
}
return index; // 非目标值个数即数组长度
}
}
复杂度分析
- 时间复杂度: O(n),两次线性遍历(统计 + 重写)。
- 空间复杂度: O(1),依旧是原地操作。
解法 4:链表解法(当输入为链表时)
如果输入是链表而不是数组,移除目标值时可以使用“指针删除”,无需双指针。
- 核心思路:
- 遍历链表,跳过所有等于
val
的节点,通过调整前驱节点的next
跳过符合条件的节点。 - 引入一个哑节点简化边界情况的处理。
- 遍历链表,跳过所有等于
模板代码
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy, cur = head;
while (cur != null) {
if (cur.val == val) {
prev.next = cur.next; // 删除当前节点
} else {
prev = cur; // 前驱节点前移
}
cur = cur.next; // 当前节点前移
}
return dummy.next;
}
}
复杂度分析
- 时间复杂度: O(n),遍历链表一次。
- 空间复杂度: O(1),只调整指针顺序。
经典变体问题
变体 1:移除所有值小于 val
的元素
在数组中移除所有小于指定值 val
的元素,保留其他元素。
思路:
- 双指针法(快慢指针,根据条件修改数组)。
模板代码
class Solution {
public int removeLessThan(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] >= val) {
nums[slow++] = nums[fast]; // 保留大于等于 `val` 的元素
}
}
return slow;
}
}
变体 2:移除重复元素
移除数组中重复的元素,例如将 [1,1,2,2,3]
变为 [1,2,3]
。
思路:
- 快慢指针法,
slow
指针记录结果数组,fast
用于遍历。 - 在重复元素时跳过。
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int slow = 0;
for (int fast = 1; fast < nums.length; fast++) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1; // 新长度
}
}
变体 3:移除 K 个范围内的元素
移除数组中值在 [low, high]
范围内的元素。
思路:
- 改造
removeElement
,在条件中加入区间判断即可。
模板代码
class Solution {
public int removeElementInRange(int[] nums, int low, int high) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] < low || nums[fast] > high) {
nums[slow++] = nums[fast];
}
}
return slow;
}
}
快速 AC 总结
- 首先掌握核心解法(双指针法,快慢指针)。
- 熟练理解变体:换条件(小于值、重复元素、区间范围)时仍然可以用解法模板。
- 对应场景(数组 vs 链表)选择合适实现。例如链表用修改指针操作更方便。
- 高效背模板:模板结构固定,快速套用并扩展到变体问题。