当前位置: 首页 > news >正文

【算法】用“龟兔赛跑”的思想原地移除元素

文章目录

  • 一、问题
  • 二、题目分析
  • 三、算法实现
    • 3.1、双指针(快慢指针)
    • 3.2、双指针(首尾双指针)
    • 3.3、无耻做法
  • 四、总结

一、问题

给定一个整数数组 nums 和一个整数值 val,你的目标是原地(in-place)移除 nums 中所有数值等于 val 的元素,可以改变元素的顺序。你需要返回移除后数组中与 val 不同的元素的数量,我们称之为 k

强调 “原地” 操作,即不允许使用额外的空间。

约束条件:

  • 原地操作: 必须在原始数组 nums 上进行修改,不能使用额外的数组空间(即空间复杂度为 O(1))。
  • 顺序改变: 元素的顺序可以改变。
  • 返回值 k: 返回值为 k,表示 nums 中不等于 val 的元素数量。
  • 数组内容要求: 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要(可以随意修改)。

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,,]
解释:函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。在返回的 k 个元素之外留下了什么并不重要。

示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,,,_]
解释:函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。在返回的 k 个元素之外留下了什么并不重要。

二、题目分析

这个问题考验对原地操作、数组元素移动以及双指针法的理解。关键在于正确地运用双指针法,并充分利用题目中 “元素顺序可以改变” 和 “nums 的其余元素和 nums 的大小并不重要” 这两个条件,来选择最合适的算法,并优化算法的效率。

输入和输出:

  • 输入: 一个整数数组 nums 和一个整数 val
  • 输出: 一个整数 k,表示 nums 中不等于 val 的元素数量。 数组 nums 也会被修改。

核心问题:

  • 如何在 O(1) 的空间复杂度下,高效地移动数组元素,使得不等于 val 的元素集中在数组的前面?
  • 如何确保返回的 k 值是正确的?

思路:

  • 双指针法: 利用两个指针,一个快指针用于遍历数组,一个慢指针用于指向下一个需要被赋值的位置。
    • 快指针 (fast pointer): 扫描整个数组,检查每个元素是否等于 val
    • 慢指针 (slow pointer): 指向下一个要放置 val 元素的位置。只有当快指针遇到 val 元素时,才将该元素移动到慢指针的位置,并更新慢指针。
  • 另一种双指针法 (如果允许改变相对顺序): 首尾双指针,可以提高在特定情况下的效率。
    • left: 指向数组的起始位置。
    • right: 指向数组的末尾位置。
    • 如果 nums[left] == val,则将 nums[left]nums[right] 交换,并且 right--。 否则, left++
    • left > right 时,停止交换。 最终,left 的值就是 k

约束解读:

  • “元素的顺序可以改变” 允许我们使用一些技巧,例如首尾双指针法,这样可以更快速地进行元素的交换。如果不允许改变顺序,则需要使用另一种双指针方法,保证元素的相对顺序。
  • nums 的其余元素和 nums 的大小并不重要” 这意味着我们不需要关心 nums 的后半部分是什么,只要保证 nums 的前 k 个元素是正确的即可。这简化了问题,因为我们不必真正地删除数组元素,只需要覆盖它们即可。

复杂度分析 (初步):

  • 时间复杂度: O(n),因为需要遍历整个数组。
  • 空间复杂度: O(1),因为是原地操作。

特殊情况考虑:

  • 如果 nums 为空数组,则返回 0。
  • 如果 nums 中没有元素等于 val,则返回 nums 的长度。
  • 如果 nums 中所有元素都等于 val,则返回 0。

在这里插入图片描述

三、算法实现

3.1、双指针(快慢指针)

利用两个指针,一个快指针用于遍历数组,一个慢指针用于指向下一个需要被赋值的位置。

  • 快指针 (fast pointer): 扫描整个数组,检查每个元素是否等于 val
  • 慢指针 (slow pointer): 指向下一个要放置 val 元素的位置。只有当快指针遇到 val 元素时,才将该元素移动到慢指针的位置,并更新慢指针。
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0, right = 0;
        while (right < nums.size()) {
            if (nums[right] != val) {
                nums[left] = nums[right];
                ++left;
            }
            ++right;
        }
        return left;
    }
};

这样的算法在最坏情况下(输入数组中没有元素等于 val),左右指针各遍历了数组一次。

快慢指针原理可以理解成一段爱情故事:

有一对夫妇, slow和fast,他们的人生原本都是零起点,对未来充满着迷茫。但生活总要继续,他们开始了各自的旅程。

Fast天生就是一个闲不住的人,他跑得飞快,探索着生活的无限可能。Slow则显得有些慢热,他需要时间来沉淀和思考。

Fast先出发了,他肩负着为两人寻找共同目标的重任。一路上,他寻找、尝试,经历着各种各样的事情 ( nums[fast] != val ) 。当他遇到一个真正有价值的目标时 ( nums[fast] != val ),他会小心翼翼地将它交给Slow ( nums[slow] = nums[fast] ) 。

Slow接收到Fast传递过来的目标,将它视为珍宝,也向前移动了一步,向着目标更近了一步 ( slow++ )。

不管Fast是否找到了目标,他都从未停下脚步,一直奔跑到生命的尽头 (fast++)。

最终,完成了筛选、并且不断前进的,只剩下了Slow。他继承了Fast的努力和付出,成为了他们共同旅程的见证者 ( return slow )。

3.2、双指针(首尾双指针)

首尾双指针,可以提高在特定情况下的效率:

  • left: 指向数组的起始位置。
  • right: 指向数组的末尾位置。
  • 如果 nums[left] == val,则将 nums[left]nums[right] 交换,并且 right--。 否则, left++
  • left > right 时,停止交换。 最终,left 的值就是 k
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0, right = nums.size();
        while (left < right) {
            if (nums[left] == val) {
                --right;
                std::swap(nums[left], nums[right]);
            } else {
                ++left;
            }
        }
        return left;
    }
};

这样的方法两个指针在最坏的情况下合起来只遍历了数组一次。与前面的快慢指针方法不同的是,它避免了需要保留的元素的重复赋值操作。

3.3、无耻做法

有题我不读,哎~就是玩。用一个临时数组保存不等于 val 的元素,最后再和 nums 交换。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        vector<int> tmp;
        tmp.reserve(nums.size());
        int count = 0;
        for (auto& num : nums) {
            if (num != val) {
                tmp.emplace_back(num);
                ++count;
            }
        }
        tmp.swap(nums);
        return count;
    }
};

四、总结

本题的核心思想是利用双指针法原地移除数组中的特定元素。通过维护两个指针,fastslow,来高效地遍历数组并实现元素的筛选和移动。

该算法的核心运作机制在于:当fast指针找到一个不等于目标值 val 的元素时,便将其赋值给 slow 指针指向的位置,然后 slow 指针向前移动一步。这样,slow 指针之前的元素始终是不等于 val 的元素,从而保证了原地移除元素的效果。

通过这种巧妙的双指针协同工作,算法在 O(n) 的时间复杂度和 O(1) 的空间复杂度内解决了问题,实现了高效且节省空间的元素移除操作。 关键在于理解 fast 指针用来遍历所有元素,而slow 指针只在发现有效的、需要保留的元素时才移动。

在这里插入图片描述

相关文章:

  • Go Context包详解与最佳实践
  • Vue学习笔记集--六大指令
  • f-string高级字符串格式化与string Template()
  • NestJS(基于 Express 的现代化框架)
  • coze ai assistant Task 3
  • 主流区块链
  • 人工智能在现代科技中的应用和未来发展趋势。
  • 每日Attention学习27——Patch-based Graph Reasoning
  • 来自腾讯的:《详解DeepSeek:模型训练、优化及数据处理的技术精髓》
  • 3.16学习总结
  • C#开发笔记:INI文件操作
  • 三、重学C++—CPP基础
  • Tsfresh + TA-Lib + LightGBM :A 股市场量化投资策略实战入门
  • Suno的对手Luno:AI音乐开发「上传参考音频 - 方式二:通过URL的方式」 —— 「Luno Api系列|AI音乐API」第12篇
  • 程序地址空间:深度解析其结构,原理与在计算机系统中的应用价值
  • 【Linux进程通信】————匿名管道命名管道
  • 超详细kubernetes部署k8s----一台master和两台node
  • 【网络】简单的 Web 服务器架构解析,包含多个服务和反向代理的配置,及非反向代理配置
  • Java学习------初识JVM体系结构
  • 格雷码.
  • 五一假期首日,多地党政主官暗访督查节日安全和值班值守工作
  • 北部艳阳高照、南部下冰雹,五一长假首日上海天气很“热闹”
  • “80后”杨占旭已任辽宁阜新市副市长,曾任辽宁石油化工大学副校长
  • 结婚这件事,年轻人到底怎么想的?
  • 发挥全国劳模示范引领作用,加速汽车产业电智化转型
  • 京津冀“飘絮之困”如何破解?专家坦言仍面临关键技术瓶颈