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

该博客介绍了如何在LeetCode上解决移除元素的问题。通过遍历数组并移除所有等于指定值的元素,然后返回新数组的长度,实现了原地修改数组的功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述:

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

解析:

根据题目说明,我们可以设想,最终返回的只需要是个整数即可,因为函数已经封装了返回的数组。所以,我们希望设置一个数组,让满足条件的值存到里面,不满足的就不存,这个很容易实现,只需要循环判断即可,最终要的值是把它的长度返回出来,题目很简单

实现代码:

class Solution {
    public int removeElement(int[] nums, int val) {
        int res = 0;
        for(int i = 0 ; i < nums.length ; i++){
            if(nums[i] == val){
                
            }else{
                nums[res] = nums[i];
                res++;
            }
        }
        return res;
    }
}
<think>好的,我现在需要解决这个问题:原地移除数组中所有等于val元素返回等于val元素的数量k,且前k个元素是那些不等于val元素。我需要用C++来实现。 首先,我得回忆一下类似的数组处理问题。之前学过双指针法,可能适用这种情况。例如,快慢指针,快指针遍历数组,慢指针记录有效位置。当快指针找到不等于val元素时,就放到慢指针的位置,然后慢指针移动。这样最后慢指针的位置就是k的。 那具体应该怎么做呢?比如说,初始时慢指针指向第一个元素,快指针从第一个开始遍历。当快指针指向的元素等于val时,就把这个复制到慢指针的位置,然后慢指针加一。这样遍历结束后,前k个元素都是有效的,剩下的不管了。这应该可以满足题目要求。 不过需要注意边界情况,比如数组为空,或者所有元素都是val,或者没有元素val的情况。比如当所有元素都是val时,快指针一直走,慢指针不动,最后k是0。如果数组为空,直接返回0。 然后,我需要把这个思路写成代码。使用两个指针,一个快指针i,一个慢指针k。初始化k为0。循环i从0到n-1,每次如果nums[i]不等于val,就把nums[k] = nums[i],然后k++。最后返回k。 那这样写的话,时间复杂度是O(n),空间复杂度是O(1),满足原地修改的要求。应该没问题。 测试几个例子看看。比如nums = [3,2,2,3], val=3,处理后前两个元素应该是2,2,返回2。另一个例子是nums = [0,1,2,2,3,0,4,2], val=2,处理后前k个应该是0,1,3,0,4,返回5。这样是否正确? 再想,有没有其他方法?比如交换元素,但可能更复杂。双指针覆盖的方法应该更直接。 写代码时,注意数组的索引是否正确。例如,初始k=0,i从0开始,每次nums[i]不等于val时,复制到k的位置,然后k自增。这样最后k就是有效元素的个数。 然后检查题目中的要求,是否允许改变顺序。题目说顺序可以改变,所以不需要保持原来的顺序。这样可能还有其他方法,比如把等于val元素交换到数组末尾。但这样可能增加复杂度,而双指针覆盖的方法更高效,不需要交换,直接覆盖即可。不过题目允许顺序改变,所以是否还有更优的方法? 比如,当要移除元素很少时,可以考虑交换。例如,当val出现次数很少时,可以用两个指针,一个从头开始,一个从尾开始,当左指针找到val时,右指针的元素交换,右指针左移。这样交换的次数等于val出现的次数。这种情况下,可能减少复制的次数。例如,当val元素很少时,这种方法可能更好。但题目没有说明元素的顺序是否可以改变,但题目说明顺序可以改变。所以这种情况下,可以采用这种方法,减少复制次数。 例如,这样的实现可能更高效。比如初始化左指针left=0,右指针right=n-1。当左指针的元素等于val时,用右指针的元素替换,右指针减一,同时左指针不动(因为替换过来的元素可能也是val)。否则左指针加一。循环直到左指针超过右指针。此时,左指针的就是k的。或者,需要统计数量?或者right的位置? 比如,当用这种方法时,循环条件是left <= right。每次如果nums[left] == val,那么将nums[left] = nums[right],然后right--。这样相当于把后面的元素覆盖当前的位置,不管是否等于val。但是这样的话,最后的有效元素数量应该是right + 1?或者left的位置? 可能需要例子来验证。例如,nums = [3,2,2,3], val=3。初始left=0,right=3。nums[0]=3等于val,将nums[0]替换为nums[3]=3,然后right=2。此时数组变为[3,2,2,3]。然后检查left=0,nums[0]还是3,替换为nums[2]=2,数组变为[2,2,2,3],right=1。现在left=0,检查是否等于val,不是,left++到1。现在left=1 <= right=1。nums[1]=2不等于val,left++到2。循环结束。此时有效元素数量是right +1=2,对吗?是的,正确。返回2。而原数组的前两个元素是2,2,正确。 另一个例子,nums=[0,1,2,2,3,0,4,2], val=2。初始left=0,right=7。left=0的元素0不等于val,left++到1。元素1不等于val,left++到2。此时nums[2]=2等于val,所以替换为nums[7]=2。数组变为[0,1,2,2,3,0,4,2],然后right=6。此时left=2,检查nums[2]=2等于val,替换为nums[6]=4,数组变为[0,1,4,2,3,0,4,2],right=5。现在检查nums[2]=4不等于val,left++到3。nums[3]=2等于val,替换为nums[5]=0,数组变为[0,1,4,0,3,0,4,2],right=4。此时检查nums[3]=0,不等于val,left++到4。nums[4]=3不等于val,left++到5。此时left=5,right=4,循环结束。有效数量是right+1=5,正确。前五个元素是0,1,4,0,3。顺序可能改变,但题目允许。所以结果正确。 所以这种方法的循环条件是left <= right,每次当nums[left]==val时,用nums[right]替换,right--。否则left++。最后返回的k是left的?或者right+1?因为当循环结束时,left的位置是right+1。例如,当所有元素都不等于val,left会一直增加到n,而right保持n-1。此时k是n,正确。当所有元素等于val,那么每次替换后right减一,最后left=0,right=-1,返回0。正确。所以此时k=right+1?或者 left?比如,在第一个例子中,循环结束时left=2,right=1,所以right+1=2。是的,正确。所以返回right+1即可。 这样的话,这种方法的时间复杂度同样是O(n),但可能减少元素的复制次数。例如,当val元素很少时,只需要交换次数等于val的出现次数。例如数组长度n,val出现t次,那么覆盖的次数是t次,而快慢指针方法需要n次判断,每次可能需要覆盖n-t次。所以当t很小时,这种方法更好。比如原数组大部分元素保留,只有少量需要移除的情况下,这种方法更优。但两种方法的时间复杂度都是O(n),差别可能在常数项。 题目没有特别说明哪种情况更优,所以两种方法都可以。但题目要求的是“元素的顺序可能发生改变”,所以两种方法都符合条件。那么可能选择哪种方法更合适? 需要比较这两种方法的优劣。快慢指针方法会保持非val元素的相对顺序,而交换方法会改变顺序。例如,在快慢指针法中,所有非val元素按原顺序出现在前k个位置;而交换方法可能打乱顺序。题目允许顺序改变,所以两种方法都正确。但可能需要根据题目是否有隐含的顺序要求来选择。但题目明确说明顺序可以改变,所以两种方法都正确。 那么现在,我需要写哪一种方法的代码?比如,题目可能更倾向于哪种? 比如,在LeetCode上,这个问题的标准解法通常是快慢指针,因为不需要改变顺序,但题目允许改变顺序的情况下,交换法可能更高效。或者,可能两种都是可行解。例如,官方题解可能有这两种方法。 比如,对于题目27. 移除元素,两种方法都是可行的。快慢指针法(同向双指针)对撞指针法(相向双指针)。 现在,我需要根据问题要求,给出一个正确的C++程序。那么可以给出两种方法中的一种,或者都给出? 用户可能希望得到一个高效的、常见的解法。例如,快慢指针法可能更直观,且保持顺序(虽然题目允许顺序改变),但可能用户希望不管顺序。或者,可能用户希望尽可能少的复制操作。 例如,快慢指针法的代码: int removeElement(vector<int>& nums, int val) { int k = 0; for (int i=0; i<nums.size(); ++i) { if (nums[i] != val) { nums[k] = nums[i]; ++k; } } return k; } 这种方法的时间复杂度是O(n),空间O(1)。对于数组中的所有元素,每个元素被赋一次(当i >=k时,nums[i]被复制到前面)。但当数组中有大量非val元素时,这样的复制可能是冗余的。例如,当所有元素都不等于val时,每个元素都会被复制到自己原来的位置,也就是无用的操作。但由于赋是不可避免的,这可能不影响整体时间复杂度。 另一种交换法: int removeElement(vector<int>& nums, int val) { int left = 0; int right = nums.size() -1; while (left <= right) { if (nums[left] == val) { nums[left] = nums[right]; right--; } else { left++; } } return left; } 或者,另一种写法,比如交换nums[left]nums[right]。但这样可能更复杂。 比如,正确的交换法: int removeElement(vector<int>& nums, int val) { int left = 0; int right = nums.size(); while (left < right) { if (nums[left] == val) { nums[left] = nums[right-1]; right--; } else { left++; } } return left; } 例如,这样当left < right时循环。每次当nums[left]等于val时,将后面的元素复制过来,且减少右边界。这样,最后left的就是k。 比如,测试例子: nums = [3,2,2,3], val=3。第一次循环,left=0,nums[0]=3,替换为nums[3-1=2],即nums[2]=2。数组变为[2,2,2,3]。right变为2。此时left=0,再次检查nums[0]=2不等于val,left++到1。nums[1]=2不等于val,left++到2。此时left=2不小于right=2,退出循环。返回left=2。正确。 另一个例子,nums=[2], val=3。循环中nums[0]=2不等于val,left++到1,循环结束,返回1。正确。而如果val=2的话,nums[0]等于val,替换为nums[0] = nums[0](因为 right=1-1=0),然后right减到-1。循环条件left < right(0 < 0)不成立,返回left=0。正确。 所以这个版本的交换法是否正确? 可能需要更多的测试案例。例如,nums=[0,1,2,2,3,0,4,2], val=2。初始left=0,right=8。进入循环,left=0,nums[0]=0不等于val,left++到1。nums[1]=1不等于val,left++到2。nums[2]=2等于val,此时替换为nums[right-1=7-1=7-1?初始right是数组大小是8,所以初始right=8。当第一次处理left=2时,nums[2]=2等于val,替换为nums[right-1=7],即nums[7]=2。数组变为[0,1,2,2,3,0,4,2]。然后right减到7-1=7?或者原right是8,那么right-1是7,所以替换后right=7。然后循环继续,检查left=2。此时nums[2]=2还是等于val,替换为nums[6](因为 right=7,right-1=6),即nums[6]=4。数组变为[0,1,4,2,3,0,4,2]。right减到6。再次检查left=2,nums[2]=4不等于val,left++到3。nums[3]=2等于val,替换为nums[5](right=6-1=5),即nums[5]=0。数组变为[0,1,4,0,3,0,4,2]。right减到5。left=3,nums[3]=0不等于val,left++到4。nums[4]=3不等于val,left++到5。此时left=5 < right=5?不成立,循环结束。返回left=5。正确,因为前五个元素是0,1,4,0,3,其他不管。所以返回5是正确的。 那这个方法是正确的。这种方法的时间复杂度是O(n),但每个等于val元素最多被处理一次。例如,当val元素很少时,只需要处理t次,剩下的非val元素不需要移动。这可能比快慢指针法更高效,特别是在val元素较少的情况下。例如,数组很大,但需要移除元素很少,这种方法只需复制几次,而快慢指针法需要复制n-t次。所以在这种情况下更优。 因此,两种方法都是可行的,取决于具体情况。那么,用户可能需要哪一种? 题目要求中,元素的顺序可以改变,所以两种方法都可以。因此,可以选择其中一种来编写代码。但为了代码的简洁性,可能快慢指针法更容易理解,而交换法可能在特定情况下更高效。 根据LeetCode的讨论,这两种方法都是可接受的。例如,官方题解中的方法一方法二分别是这两种。 现在,我需要选择一个合适的写法。例如,用户可能希望得到两种方法的说明,或者其中一种。 但根据用户的问题,给出一个正确的C++程序即可。可能选择快慢指针法更直观,而交换法则可能更高效但稍微难理解一些。 现在,我需要写出正确的代码。例如,快慢指针法: class Solution { public: int removeElement(vector<int>& nums, int val) { int k = 0; for (int x : nums) { if (x != val) { nums[k++] = x; } } return k; } }; 或者,使用for循环的索引: int removeElement(vector<int>& nums, int val) { int k = 0; for (int i = 0; i < nums.size(); ++i) { if (nums[i] != val) { nums[k] = nums[i]; k++; } } return k; } 这两种写法是等价的。其中第一种写法用了范围for循环,但需要注意在遍历时是否修改数组是否会影响循环。不过在这种情况下,因为k<=i,所以不会覆盖未处理的元素。例如,假设nums是[3,2,2,3], val=3。当i=0时,元素是3,不处理,k=0。i=1时,元素是2,nums[0]=2,k=1。i=2时,元素2,nums[1]=2,k=2。i=3时,元素3,不处理。循环结束,返回k=2。正确。 而使用范围for循环时,x是nums中的元素的副本,所以不会影响原数组的修改。例如,在循环中,nums元素可能被修改,但x的是原数组中的。例如,当nums元素被修改时,后面的x的可能已经改变。例如,在交换法中,这可能有问题,但在快慢指针法中,因为k<=i,所以覆盖的元素都是已经处理过的位置,所以不会影响到后续的循环。例如,i是遍历原数组的所有元素,而k记录的是当前应保留的位置。所以即使修改前面的元素,后面的遍历i会继续处理原数组后面的元素,所以不会影响结果。因此,这种写法是正确的。 不过,使用范围for循环中的x,其实是遍历原数组元素,而快慢指针法中,修改的是数组前面的元素,但是不影响i的遍历,因为i是索引,从0到nums.size()-1。而k的索引始终<=i,所以不会覆盖后面未处理的元素。所以这两种写法都是正确的。 综上所述,正确的快慢指针法代码如上。而交换法的代码如下: int removeElement(vector<int>& nums, int val) { int left = 0; int right = nums.size(); while (left < right) { if (nums[left] == val) { nums[left] = nums[right - 1]; right--; } else { left++; } } return left; } 这两种方法都是正确的。现在,根据用户的需求,可能需要其中一种。例如,用户可能希望更高效或者更直观的代码。因此,给出两种方法的说明可能更好,但用户可能只需要一种。根据问题描述,用户需要“写一个C++程序”,所以给出一种正确的即可。 可能快慢指针法更直观,适用于保留顺序的情况,虽然题目允许改变顺序。而交换法则更高效,但改变了顺序。根据题目的示例,可能原题希望使用快慢指针法,例如在LeetCode中,这个问题(27. Remove Element)的标准答案通常给出这两种方法。 因此,在回答时,可以给出两种方法,说明它们的区别。但用户的问题可能只需要一个正确的程序,所以选择其中一种即可。例如,选择快慢指针法,因为代码更简洁,且更直观,容易理解。 综上,我会选择快慢指针法来编写代码,给出相应的解释。</think>### 原地移除元素的C++实现及解析 以下为解决该问题的两种常用方法,均满足时间复杂度 $O(n)$ 空间复杂度 $O(1)$: --- #### 方法一:快慢指针法(保留原有顺序) **核心思想**:通过双指针遍历,将非目标元素依次前移,保证有效元素的原始顺序。 ```cpp #include <vector> using namespace std; int removeElement(vector<int>& nums, int val) { int k = 0; // 慢指针,记录有效元素位置 for (int x : nums) { if (x != val) { // 发现有效元素 nums[k++] = x; // 前移指针 } } return k; } ``` **执行流程**: 1. 快指针隐式遍历数组(通过循环变量 `x`) 2. 慢指针 `k` 仅在遇到非 `val` 元素时移动 3. 最终 `k` 即为有效元素数量 **示例**: 输入:`nums = [3,2,2,3], val = 3` 输出:`k = 2`,`nums` 前两元素变为 `[2,2]` --- #### 方法二:对撞指针法(允许改变顺序) **核心思想**:通过交换减少赋次数,将目标元素替换到数组末尾。 ```cpp #include <vector> using namespace std; int removeElement(vector<int>& nums, int val) { int left = 0; // 左指针 int right = nums.size(); // 右指针(开区间) while (left < right) { if (nums[left] == val) { // 发现目标 nums[left] = nums[--right]; // 用末尾元素替换 } else { left++; // 非目标则保留 } } return left; // left即为有效元素个数 } ``` **执行流程**: 1. 左指针 `left` 从首部开始扫描 2. 右指针 `right` 动态标记有效区间末尾 3. 当发现 `val` 时,用末尾元素覆盖当前缩小右边界 4. 最终 `left` 即为有效元素数量 **示例**: 输入:`nums = [0,1,2,2,3,0,4,2], val = 2` 输出:`k = 5`,前五元素可能为 `[0,1,4,0,3]` --- ### 关键点对比 | 特性 | 快慢指针法 | 对撞指针法 | |--------------------|-----------------------------|---------------------------| | 元素顺序 | 保留原有顺序 | 可能改变顺序 | | 赋次数 | 等于有效元素数量 | 等于目标出现次数 | | 适用场景 | 需保留顺序时 | 无需保留顺序时更高效 | | 时间复杂度 | $O(n)$ | $O(n)$ | --- ### 边界条件测试 1. **全为目标**:`nums = [3,3,3], val = 3` → 返回 `0` 2. **空数组**:`nums = [], val = 0` → 返回 `0` 3. **首尾为目标**:`nums = [2,1,3,4,2], val = 2` → 返回 `3`
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值