27.移除元素

214 阅读3分钟

题目:

给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素处。

示例

输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2] 解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案

思路一


根据题目我们可以简化算法的核心求解步骤为如下两步:

  • 第一步:找到要移除元素的数组下标
  • 第二步:移除该元素并更新数组长度

因此可以使用暴力求解法,即使用双层for循环,外层负责实现第一步,内层负责实现第二步。

思路二


根据题目我们可以简化算法的核心求解步骤为如下两步:

  • 第一步:找到非目标元素的数组下标(即不被移除的元素)
  • 第二步:将该非目标元素移入新数组中(实际上该新数组任是在旧数组上进行元素的移动,保证空间复杂度为o(1))

因此可以通过双指针法(快慢指针法):即通过一个快指针和一个慢指针实现在一个for循环内完成两个for循环的工作,相较于暴力求解时间复杂度要小的多。

关键点回忆


  • 因为数组的实现方式使得它的元素在逻辑上和物理上都是连续存放的,所以不存在单独删除某个元素,所以数组中删除和插入元素本质上都是通过元素的覆盖实现的。

  • 双指针法(这里用的是快慢指针法)你可以简单的理解为,是一种和用双层for循环这种暴力解法思想相反的一种解法,它是通过寻找非移除元素,并把非移除元素移入到一个新数组中(通过双指针的方式本质上还是在旧数组上操作),来间接的实现了移除目标元素的目的。

  • c++中vector.erase()方法的内部实现就是通过类似双指针法的这种方式实现的。

c++代码实现一-暴力解法

#include <iostream>
#include <vector>

using namespace std;

// 第一种暴力解法
class Solution {
    public:
        int removeElement(vector<int>& nums, int val){
            int size = nums.size(); //数组当前长度
            
            // 第一层for循环通过遍历找到要移除元素的数组下标
            for (int i = 0; i < size; i++) {
                if(nums[i] == val){//判断当前元素是否为移除元素
                    
                    //第二层for循环实现移除元素的操作,将数组下标为i+1到size的所有元素集体先前移动一位
                    for (int j = i + 1; j < size; j++) {
                        nums[j-1] = nums[j];//后一位元素依次向前覆盖
                    }

                    i--; //因为上一步前移元素的时候数组下标为i的位置也被后一个元素覆盖了,故该元素在下次循环中仍要内再次判断
                    size--; //移除一个元素,数组长度减一
                } 

            }
            return size; //返回最终移除目标元素后数组的长度
        }
};

int main(){
    //测试用例
    /*输入:nums = [3,2,2,3], val = 3
    输出:2, nums = [2,2]*/
    //声明一个vector容器
    vector<int> nums = {3,2,2,3};
    Solution s1;
    cout << s1.removeElement(nums, 3);
    return 0;
}
  • 时间复杂度 O(n2)O(n^2)
  • 空间复杂度 O(1)O(1)

C++代码实现二-双指针法(快慢指针法)

#include <iostream>
#include <vector>

using namespace std;

//第二种:双指针法-快慢指针法
class Solution {
    public:
        int removeElement(vector<int>& nums, int val) {
            int slowIndex = 0; //指向当前新数组末尾元素的后一个位置,新数组初始时尾元素下标为-1
            //通过for循环遍历,找到当前非目标值的元素(即新数组中的元素),其中fastIndex的目的是为了指向新数组元素
            for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {

                //通过if判断获取非目标值元素(即新数组元素)
                if (val != nums[fastIndex]) 
                    nums[slowIndex++] = nums[fastIndex]; //将非目标值元素放入新数组中,并且solwIndex加一
            }
            return slowIndex; //返回新数组的长度
        }
};

int main(){
    //测试用例
    /*输入:nums = [3,2,2,3], val = 3
    输出:2, nums = [2,2]*/
    //声明一个vector容器
    vector<int> nums = {3,2,2,3};
    Solution s1;
    cout << s1.removeElement(nums, 3);
    return 0;
}
  • 时间复杂度 O(n)O(n)
  • 空间复杂度 O(1)O(1)

总结

  • 当我们面对通过数组或者链表求解的算法问题时,可以用双指针法来代替需要用到双层循环的部分,可以大幅降低算法的时间复杂度。