文章参考:
本文仅供学习使用,若侵权删,若有问题敬请批评指出!
题目
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。(原地移除即不使用额外的空间来创建新的数组)
假设 nums 中不等于 val 的元素数量为 k,要通过此题,需要执行以下操作:
- 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
- 返回 k。
测试
评测机将使用以下代码测试您的解决方案:
int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
// 它以不等于 val 的值排序。
int k = removeElement(nums, val); // 调用你的实现
assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i < actualLength; i++) {
assert nums[i] == expectedNums[i];
}
如果所有的断言都通过,你的解决方案将会通过。
这段代码是一个用于测试移除数组中特定值的函数 removeElement
的示例。它通过一系列断言(assertions)来验证 removeElement
函数的行为是否符合预期。下面是对代码的详细解析:
int[] expectedNums = [...]
:
定义了一个整数数组 expectedNums
,表示在移除所有等于 val
的元素后,期望得到的结果数组。这个数组中的元素应该已经按升序排列,并且不包含任何等于 val
的元素。
assert k == expectedNums.length
:
断言 removeElement
方法返回的新长度 k
应该与 expectedNums
的长度相等。如果 k
不等于 expectedNums.length
,则抛出异常,表明测试失败。
assert nums[i] == expectedNums[i]
:
在每次迭代中,断言 nums
的第 i
个元素应该与 expectedNums
的第 i
个元素相等。如果有任何一个元素不匹配,则抛出异常,表明测试失败。
解题思路
1. 暴力法(Brute Force)
即通过逐个检查每个元素是否等于 val
,并在找到匹配项时将其后的所有元素向前移动一位来覆盖掉该元素。
class Solution {
public int removeElement(int[] nums, int val) {
// 暴力法
int n = nums.length; // 获取数组的初始长度
for (int i = 0; i < n; i++) { // 遍历数组中的每一个元素
if (nums[i] == val) { // 如果当前元素等于目标值 val
for (int j = i + 1; j < n; j++) { // 内层循环将后续元素前移一位
nums[j - 1] = nums[j];
}
i--; // 因为删除了一个元素,所以需要重新检查当前位置的元素
n--; // 数组的有效长度减一
}
}
return n; // 返回新的有效长度
}
}
2. 双指针法
这是解决此类问题最常用的方法之一。通过使用两个指针(快慢指针),可以在一次遍历中完成移除操作。
- 快指针:用于遍历整个数组。
- 慢指针:用于记录需要保留的元素位置。
步骤:
- 初始化两个指针
i
和j
,都指向数组的起始位置。 - 遍历数组:
- 如果当前元素不等于要移除的值
val
,将该元素复制到慢指针的位置,并移动慢指针。 - 如果当前元素等于
val
,则跳过该元素,只移动快指针。
- 如果当前元素不等于要移除的值
- 最终,慢指针的位置即为新数组的有效长度。
public int removeElement(int[] nums, int val) {
int j = 0; // 慢指针
for (int i = 0; i < nums.length; i++) { // 快指针遍历数组
if (nums[i] != val) {
nums[j] = nums[i];
j++;
}
}
return j;
}
3. 交换元素法
这种方法适用于不需要保持原始顺序的情况。它通过从数组末尾找到非目标值并将其与目标值交换来减少遍历次数。
步骤:
- 使用两个指针,一个指向数组开头
start
,另一个指向数组结尾end
。 - 当
start
指向的元素等于val
时,检查end
指向的元素是否也等于val
:- 如果
end
指向的元素不等于val
,交换这两个位置的元素,并向前移动start
和向后移动end
。 - 如果
end
指向的元素等于val
,仅向后移动end
。
- 如果
- 当
start
和end
相遇或交错时结束循环,此时start
的位置即为新数组的有效长度。
public int removeElement(int[] nums, int val) {
int start = 0, end = nums.length - 1;
while (start <= end) {
if (nums[start] == val) {
if (nums[end] != val) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
} else {
end--;
}
} else {
start++;
}
}
return start;
}
4. 过滤法
对于允许创建新数组或对空间复杂度没有严格限制的情况,可以使用过滤方法。这种方法简单但不符合题目要求的“原地”修改条件。
步骤:
- 创建一个新的列表或数组。
- 遍历原数组,将所有不等于
val
的元素添加到新列表或数组中。 - 返回新列表或数组的长度。
(过滤法提供了一种简单的方法来移除数组中所有等于给定值的元素。虽然它不符合“原地”操作的要求,但在理解和实现上非常直观。)
使用 Java 实现过滤法的示例:
import java.util.ArrayList;
import java.util.List;
public class Solution {
public int removeElement(int[] nums, int val) {
// 创建一个新的列表来保存非目标值的元素
List<Integer> filtered = new ArrayList<>();
// 遍历原数组,将不等于 val 的元素添加到新列表中
for (int num : nums) {
if (num != val) {
filtered.add(num);
}
}
// 将过滤后的元素复制回原数组的前 k 个位置
for (int i = 0; i < filtered.size(); i++) {
nums[i] = filtered.get(i);
}
// 返回新数组的有效长度
return filtered.size();
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {3, 2, 2, 3};
int val = 3;
int k = solution.removeElement(nums, val);
System.out.println("New length: " + k);
for (int i = 0; i < k; i++) {
System.out.print(nums[i] + " ");
}
}
}
- 空间复杂度:过滤法的空间复杂度为 O(n),因为它需要额外的空间来存储过滤后的元素。这与题目要求的“原地”操作不同,后者通常要求空间复杂度为 O(1)。
- 时间复杂度:时间复杂度为 O(n),因为我们需要遍历整个数组一次。
双指针法和交换元素法的时间复杂度都为O(n),空间复杂度都为O(1)。
双指针法和交换元素法的详细对比
1. 实现逻辑
双指针法
- 逻辑:使用两个指针遍历数组,一个快指针(通常是一个普通的
for
循环)遍历整个数组,另一个慢指针记录需要保留的元素位置。 - 操作:当快指针指向的元素不等于目标值时,将其复制到慢指针的位置,并移动慢指针。如果当前元素等于目标值,则跳过该元素,只移动快指针。
- 顺序保持:最终结果中剩余的元素将保持原始顺序。
交换元素法
- 逻辑:使用两个指针,一个指向数组开头 (
start
),另一个指向数组结尾 (end
)。 - 操作:当
start
指向的元素等于目标值时,检查end
指向的元素是否也等于目标值:- 如果
end
指向的元素不等于目标值,则交换这两个位置的元素,并向前移动start
和向后移动end
。 - 如果
end
指向的元素等于目标值,则仅向后移动end
。
- 如果
- 顺序保持:由于涉及元素交换,最终结果中剩余的元素顺序可能被打乱。
2. 适用场景
双指针法
- 保持顺序:适用于需要保持数组中剩余元素顺序的情况。例如,在某些排序算法或用户界面展示中,顺序是非常重要的。
- 简单直观:实现较为简单,易于理解和维护。
交换元素法
- 不关心顺序:适用于对顺序没有严格要求的情况。它可以更灵活地处理两端的元素,有时可以减少不必要的遍历次数。
- 优化遍历:在某些情况下,特别是数据分布较为随机的情况下,交换元素法可能会减少遍历次数,因为可以从两端同时进行处理。
3. 性能差异
时间复杂度
- 两者相同:无论是双指针法还是交换元素法,最坏情况下的时间复杂度都是 O(n),其中 n 是输入数组的长度。这是因为每个元素最多被访问两次(一次由
start
指针,一次由end
指针),因此时间复杂度仍然是线性的。
实际表现
- 双指针法:每次遇到非目标值时都会执行一次赋值操作,但这些操作是连续的,因此在缓存友好性方面表现较好。
- 交换元素法:虽然减少了遍历次数,但在某些情况下(如目标值集中在数组中间)可能会导致频繁的交换操作,这可能会增加一些开销。
空间复杂度
- 两者相同:两种方法都只需要常数级别的额外空间(O(1)),符合“原地”操作的要求。
总结
- 双指针法 更适合那些需要保持数组中剩余元素顺序的问题场景。它通过简单的遍历实现了移除操作,逻辑清晰易懂。
- 交换元素法 则更适合那些对顺序没有严格要求的情况。它可以更灵活地处理两端的元素,有时可以减少不必要的遍历次数,特别是在数据分布较为随机的情况下。
实操
自己编写代码:
双指针法:
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];
}
}
return slow;
}
}
交换元素法:
class Solution {
public int removeElement(int[] num2,int val2){
int start = 0;
int end = num2.length - 1;
while(start <= end){
if(num2[start] == val2){
if(num2[end] != val2){
num2[start] = num2[end];
start++;
end--;
}else{
end--;
}
}else{
start++;
}
}
return start;
}
}
上传至LeetCode运行提交并查看解析;
使用AI优化后的代码:
import java.util.Arrays;
/**
* LeetCode 27: 移除元素
* 给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度
* 优化点:
* 1. 添加了详细的注释说明
* 2. 优化了变量命名,使其更具可读性
* 3. 增加了输入参数的校验
* 4. 改进了测试用例的输出格式
*/
public class LeetCode2 {
public static void main(String[] args) {
// 初始化测试用例
int[] nums1 = {3, 2, 2, 3};
int val1 = 3;
int[] nums2 = {0, 1, 2, 2, 3, 0, 4, 2};
int val2 = 2;
LeetCode2 solution = new LeetCode2();
// 打印测试用例信息
System.out.println("=== 测试用例1 ===");
System.out.println("原始数组: " + Arrays.toString(nums1));
System.out.println("目标值: " + val1);
System.out.println("\n=== 测试用例2 ===");
System.out.println("原始数组: " + Arrays.toString(nums2));
System.out.println("目标值: " + val2);
// 测试双指针法
System.out.println("\n=== 双指针法结果 ===");
int[] nums1Copy = nums1.clone();
int[] nums2Copy = nums2.clone();
int newLength1 = solution.removeElement1(nums1Copy, val1);
int newLength2 = solution.removeElement1(nums2Copy, val2);
System.out.println("数组1处理结果: " + Arrays.toString(Arrays.copyOf(nums1Copy, newLength1)));
System.out.println("新长度: " + newLength1);
System.out.println("数组2处理结果: " + Arrays.toString(Arrays.copyOf(nums2Copy, newLength2)));
System.out.println("新长度: " + newLength2);
// 测试交换元素法
System.out.println("\n=== 交换元素法结果 ===");
nums1Copy = nums1.clone();
nums2Copy = nums2.clone();
int newLength3 = solution.removeElement2(nums1Copy, val1);
int newLength4 = solution.removeElement2(nums2Copy, val2);
System.out.println("数组1处理结果: " + Arrays.toString(Arrays.copyOf(nums1Copy, newLength3)));
System.out.println("新长度: " + newLength3);
System.out.println("数组2处理结果: " + Arrays.toString(Arrays.copyOf(nums2Copy, newLength4)));
System.out.println("新长度: " + newLength4);
}
/**
* 方法1: 使用双指针法移除指定元素
* 时间复杂度: O(n), 空间复杂度: O(1)
*
* @param nums 输入数组
* @param val 要移除的目标值
* @return 移除元素后的数组新长度
*/
public int removeElement1(int[] nums, int val) {
// 输入参数校验
if (nums == null || nums.length == 0) {
return 0;
}
int slowPointer = 0; // 慢指针,指向新数组的当前位置
// 快指针遍历整个数组
for (int fastPointer = 0; fastPointer < nums.length; fastPointer++) {
// 当遇到不等于目标值的元素时,将其移动到慢指针位置
if (nums[fastPointer] != val) {
nums[slowPointer] = nums[fastPointer];
slowPointer++; // 更新慢指针位置
}
}
return slowPointer; // 返回新数组的长度
}
/**
* 方法2: 使用交换元素法移除指定元素
* 时间复杂度: O(n), 空间复杂度: O(1)
* 通过首尾双指针交换元素的方式,将需要删除的元素移到数组末尾
*
* @param nums 输入数组
* @param val 要移除的目标值
* @return 移除元素后的数组新长度
*/
public int removeElement2(int[] nums, int val) {
// 输入参数校验
if (nums == null || nums.length == 0) {
return 0;
}
int left = 0; // 左指针
int right = nums.length - 1; // 右指针
while (left <= right) {
if (nums[left] == val) {
// 如果右指针指向的不是目标值,则进行交换
if (nums[right] != val) {
nums[left] = nums[right];
left++;
}
right--; // 右指针左移
} else {
// 左指针指向的不是目标值,左指针右移
left++;
}
}
return left; // 返回新数组的长度
}
}