目录

  • 零、题目描述
  • 一、为什么这道题值得你花几分钟看懂?
  • 二、题目拆解:提取其中的关键点
  • 三、明确思路:双指针的巧妙配合
  • 四、算法实现:双指针的代码演绎
  • 五、C++代码实现:一步步拆解
    • 代码拆解
    • 时间复杂度和空间复杂度
  • 六、实现过程中的坑点总结
  • 七、举一反三

零、题目描述

题目链接:移动零

题目描述:
在这里插入图片描述

示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:
输入: nums = [0]
输出: [0]

提示:
1 <= nums.length <= 10^4
-2^31 <= nums[i] <= 2^31 - 1

一、为什么这道题值得你花几分钟看懂?

如果你正在打磨自己的算法基础,那「移动零」这道题你一定不能错过——它是 LeetCode 第 283 题,更是双指针技巧的经典入门题,几乎所有算法入门教程都会拿它举例。

从算法能力提升来讲,这道题能帮你深刻理解双指针在原地修改数组中的核心作用,掌握「快慢指针配合」的精髓。这种技巧不仅能解决数组中的元素移动问题,还能延伸到链表操作、滑动窗口等多种场景,是很多高效算法的基础。

甚至在实际开发中,当需要对数据进行过滤、整理,又希望节省空间时,双指针的思路能帮你写出更高效、更优雅的代码。

二、题目拆解:提取其中的关键点

先看原题:

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。必须在不复制数组的情况下原地对数组进行操作。

再结合所给的代码框架和提示:

class Solution {
public:void moveZeroes(vector<int>& nums) {}
};

核心要素提炼:

  1. 输入是一个整数类型的向量nums,需要对其进行原地修改。
  2. 操作目标是将数组中的所有0移到末尾,同时保证非零元素的相对顺序不变。
  3. 不能使用额外的数组来辅助,必须在原数组上进行操作。

关键点:

  1. 双指针应用:用两个指针分别追踪非零元素的位置和当前遍历的位置。
  2. 元素交换:将非零元素移动到前面合适的位置。
  3. 保持顺序:确保非零元素的相对位置和原来一致。
  4. 原地操作:不使用额外的数组空间,只在原数组上进行修改。

三、明确思路:双指针的巧妙配合

1. 最直观的想法:先移非零,再补零

拿到题的第一反应:把所有非零元素按顺序移到数组前面,然后把剩下的位置都填上0。比如对于数组[0,1,0,3,12],先把非零元素1312依次移到前面,得到[1,3,12,...],再把后面的位置补成0,结果就是[1,3,12,0,0]

这种思路的关键是如何高效地找到非零元素应该放置的位置,这就需要用到双指针了。

2. 双指针的分工

在这里,我们可以用两个指针:

  • cur指针:负责遍历整个数组,寻找非零元素,相当于“探索者”。
  • dest指针:负责记录下一个非零元素应该放置的位置,相当于“占位者”。

初始时,cur从数组开头开始遍历,dest指向-1(表示还没有确定非零元素的位置)。当cur遇到非零元素时,就把dest向前移动一位,然后交换curdest指向的元素。这样,dest始终指向已经处理好的非零元素的最后一个位置。

举个例子(示例1):

初始数组:[0,1,0,3,12],cur=0,dest=-1
cur=0,元素是0,不做操作,cur++
cur=1,元素是1(非零),dest++变为0,交换nums[0]和nums[1],数组变为[1,0,0,3,12],cur++
cur=2,元素是0,不做操作,cur++
cur=3,元素是3(非零),dest++变为1,交换nums[1]和nums[3],数组变为[1,3,0,0,12],cur++
cur=4,元素是12(非零),dest++变为2,交换nums[2]和nums[4],数组变为[1,3,12,0,0],cur++
遍历结束,得到结果

3. 为什么这样可行?

因为cur指针一直在dest指针前面或者和它同步,dest指针前面的元素都是已经处理好的非零元素,并且保持着原来的相对顺序。当cur遇到非零元素时,把它交换到dest+1的位置,就保证了非零元素按原来的顺序排列在前面,而后面剩下的位置自然都是0了。

这种方法只需要遍历一次数组,并且是原地操作,时间复杂度是O(n),空间复杂度是O(1),非常高效。

四、算法实现:双指针的代码演绎

这道题我用双指针的方法来实现,下面讲解具体的算法思路。

核心逻辑:
cur指针遍历数组,当遇到非零元素时,将dest指针向前移动一位,然后交换curdest指向的元素,这样就能把非零元素逐步移到前面,零自然就到后面去了。

步骤拆解:

  1. 初始化cur为0,dest为-1。
  2. cur遍历数组中的每个元素:
    • nums[cur]是非零元素:
      • dest向前移动一位(dest++)。
      • 交换nums[cur]nums[dest]
    • cur向前移动一位(cur++)。
  3. 遍历结束后,数组中的所有零都被移到了末尾,非零元素保持相对顺序不变。

双指针细节:

  1. cur指针:从0开始,依次遍历数组的每个元素,直到数组末尾。它的作用是发现非零元素。
  2. dest指针:初始值为-1,表示还没有非零元素被放置。每当cur找到一个非零元素,dest就向前移动一位,指向这个非零元素应该放置的位置。
  3. 交换操作:当cur找到非零元素时,交换nums[cur]nums[dest],这样非零元素就被放到了前面合适的位置。
  4. 遍历顺序cur始终从左到右遍历,保证了非零元素的相对顺序不变。

五、C++代码实现:一步步拆解

完整代码(附详细注释):

#include <vector>
using namespace std;class Solution {
public:void moveZeroes(vector<int>& nums) {int n = nums.size();// cur用于遍历数组,dest用于记录非零元素应放的位置for (int cur = 0, dest = -1; cur < nums.size(); cur++) {if (nums[cur] != 0) {  // 遇到非零元素dest++;  // dest移动到下一个位置swap(nums[dest], nums[cur]);  // 交换元素,将非零元素放到前面}}}
};

代码拆解

1. 变量初始化

int n = nums.size();  // 获取数组长度,虽然这里没有直接使用,但可以用于后续扩展
int cur = 0;  // 遍历指针,从数组开头开始
int dest = -1;  // 目标指针,初始为-1,表示还没有非零元素被放置

作用cur负责遍历整个数组,dest负责跟踪非零元素应该放置的位置,初始状态下还没有非零元素被处理,所以dest为-1。

2. 主循环逻辑

for (int cur = 0, dest = -1; cur < nums.size(); cur++) {// 循环条件:cur遍历完数组的所有元素if (nums[cur] != 0) {  // 当遇到非零元素时dest++;  // 目标位置向前移动一位swap(nums[dest], nums[cur]);  // 交换元素,将非零元素放到目标位置}// 如果是零元素,则不做处理,cur继续向前移动
}

核心设计

  • 遍历机制cur从0开始,每次循环后自增1,直到遍历完整个数组,确保每个元素都被检查。
  • 非零元素处理:当nums[cur]不是0时,说明找到了一个需要放到前面的元素。此时dest先自增1,指向当前应该放置非零元素的位置,然后交换nums[cur]nums[dest],这样非零元素就被放到了前面。
  • 零元素处理:当nums[cur]是0时,不做任何操作,cur继续向前移动,零元素就被留在了原地,最终会被后面的非零元素交换到后面去。

示例走读(示例1:nums = [0,1,0,3,12])

步骤curdestnums[cur]操作数组状态
初始0-10不操作,cur++[0,1,0,3,12]
11-11dest++变为0,交换nums[1]和nums[0],cur++[1,0,0,3,12]
2200不操作,cur++[1,0,0,3,12]
3303dest++变为1,交换nums[3]和nums[1],cur++[1,3,0,0,12]
44112dest++变为2,交换nums[4]和nums[2],cur++[1,3,12,0,0]
结束5(退出循环)2--[1,3,12,0,0]

通过这个过程可以清晰地看到,非零元素1、3、12依次被放到了数组前面,零元素被移到了后面,并且非零元素的相对顺序保持不变。

时间复杂度和空间复杂度

复杂度类型具体值说明
时间复杂度O(n)n是数组的长度,cur指针遍历整个数组一次,每个元素最多被交换一次
空间复杂度O(1)只使用了两个额外的指针变量,没有使用额外的数组或其他数据结构

这种算法在时间和空间上都非常高效,符合题目的要求。

六、实现过程中的坑点总结

  1. dest指针的初始值
    错误写法

    for (int cur = 0, dest = 0; cur < nums.size(); cur++) {// ...
    }
    

    问题:如果dest初始化为0,当数组的第一个元素是非零元素时,会把它和自己交换,虽然结果正确,但做了无用功。更重要的是,如果数组全是零元素,可能会出现不必要的操作。
    正确做法:初始化为-1,让第一个非零元素能正确地放到索引0的位置。

  2. 忘记交换元素
    错误写法

    if (nums[cur] != 0) {dest++;nums[dest] = nums[cur];  // 直接赋值,没有处理原来的位置
    }
    

    问题:这样会导致原来dest位置的元素被覆盖,而cur位置的非零元素没有被清除,可能会留下重复的值。
    正确做法:使用swap函数交换两个位置的元素,确保非零元素移动的同时,零元素被换到后面。

  3. 遍历顺序错误
    错误写法

    for (int cur = nums.size() - 1; cur >= 0; cur--) {// ...
    }
    

    问题:从后往前遍历会打乱非零元素的相对顺序,无法保证移动后非零元素的顺序和原来一致。
    正确做法cur指针必须从左到右遍历数组,才能保证非零元素的相对顺序不变。

  4. 处理空数组或只有一个元素的情况
    虽然代码中没有专门处理,但当前的代码逻辑对这些情况是兼容的:

    • 如果数组为空,循环不会执行,直接返回。
    • 如果数组只有一个元素,无论是0还是非零,循环执行一次后都会得到正确的结果。
  5. 不必要的交换
    curdest相等时(比如数组开头都是非零元素),交换操作是不必要的。可以添加一个判断来优化:

    if (nums[cur] != 0) {dest++;if (cur != dest) {  // 只有当位置不同时才交换swap(nums[dest], nums[cur]);}
    }
    

    这样可以减少一些无用的交换操作,提高代码效率。

七、举一反三

学会这道题的双指针技巧,你能解决很多类似的数组操作问题:

  • LeetCode 27. 移除元素:给定一个值,移除数组中所有等于这个值的元素,思路是用双指针将不等于该值的元素移到前面。
  • LeetCode 26. 移除重复元素:删除有序数组中的重复项,让每个元素只出现一次,用双指针可以高效地实现。
  • LeetCode 80. 删除有序数组中的重复项 II:允许元素最多出现两次,双指针的思路可以扩展应用。

明天我们一起讨论LeetCode 1089. 复写零这道题有兴趣的朋友可以提前思考下

双指针技巧在数组和链表操作中非常常见,掌握它能让你在处理元素移动、删除、查找等问题时更加得心应手。

最后,欢迎大家在评论区分享自己的代码和思路,一起交流学习~ 如果你有更巧妙的方法,也请不吝赐教,我会认真学习并回复的!
在这里插入图片描述

这是今天的封面原图:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91554.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91554.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/91554.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

arrch64架构下调用pyvista报错

arrch64架构下调用pyvista报错 问题 python编程使用到了pyvista&#xff0c;使用conda新建了环境&#xff0c;但是使用的时候报错 Traceback (most recent call last):File "/home/ztl/MGGBSAR/src/trans_las_3D.py", line 16, in <module>import pyvista as p…

功能强大编辑器

时间限制&#xff1a;1秒 内存限制&#xff1a;128M题目描述你要帮助小可创造一个超级数字编辑器&#xff01;编辑器依旧运行在Linux下&#xff0c;因此你只能通过指令去操控他。指令有五种&#xff1a; In X 表示在光标左侧插入一个数字 Del 表示删除光标左侧一个数字 …

【力扣】面试经典150题总结01-数组/字符串

1.合并两个有序数组&#xff08;简单&#xff09;要求直接在num1上操作&#xff0c;已经预留了空间&#xff0c;所以直接倒着从大到小插入。当其中一个数组遍历完&#xff0c;就把另一个数组剩余的部分插入。2.移除元素&#xff08;简单&#xff09;要求原地移除数组中所有val元…

基于 Hadoop 生态圈的数据仓库实践 —— OLAP 与数据可视化(一)

目录 一、OLAP 与 Impala 简介 1. OLAP 简介 2. Impala 简介 &#xff08;1&#xff09;Impala 是什么 &#xff08;2&#xff09;为什么要使用 Impala &#xff08;3&#xff09;适合 Impala 的使用场景 &#xff08;4&#xff09;Impala 架构 &#xff08;5&#xff…

PyTorch L2范数详解与应用

torch.norm 是什么 torch.norm(dot_product, p=2, dim=-1) 是 PyTorch 中用于计算张量 L2 范数的函数, 1. 各参数解析 dot_product:输入张量,在代码中形状为 [batch_size, seq_len](每个元素是 token 隐藏状态与关注向量的点积)。 p=2:指定计算L2 范数(欧几里得范数)…

循环神经网络RNN原理精讲,详细举例!

第一部分&#xff1a;为什么需要RNN&#xff1f;在了解RNN是什么之前&#xff0c;我们先要明白它解决了什么问题。传统的神经网络&#xff0c;比如我们常见的前馈神经网络&#xff08;Feedforward Neural Network&#xff09;或者卷积神经网络&#xff08;CNN&#xff09;&…

如何用USRP捕获手机信号波形(中)手机/基站通信

目录&#xff1a; 如何用USRP捕获手机信号波形&#xff08;上&#xff09;系统及知识准备 如何用USRP捕获手机信号波形&#xff08;中&#xff09;手机/基站通信 如何用USRP捕获手机信号波形&#xff08;下&#xff09;协议分析 四、信号捕获结果 4.1 时域波形 我怀疑下面…

(LeetCode 面试经典 150 题 ) 155. 最小栈 (栈)

题目&#xff1a;155. 最小栈 思路&#xff1a;栈&#xff0c;时间复杂度0(n)。 在插入栈元素val时&#xff0c;同时加入一个字段&#xff0c;维护插入当前元素val时的最小值即可。 C版本&#xff1a; class MinStack { public:stack<pair<int,int>> st;MinStac…

算法:动态规划 洛谷 线性状态动态规划 P1439【模板】最长公共子序列

思路&#xff1a;因为n<1e5,所以不能O&#xff08;n方&#xff09;的复杂度&#xff0c;所以常规的计算最长公共子序列的方法就不行&#xff0c;不过这题有个特点&#xff0c;就是a&#xff0c;b都是排列&#xff0c;那么a有的数b也有&#xff0c;并且数量还一样&#xff0c…

Linux跑后台服务

vi /usr/lib/systemd/system/my_service.service文件配置内容&#xff1a;[Unit] Descriptionmyprogram Afternetwork.target[Service] Userroot Typesimple ExecStart/home/userabc/programs/myprogram/myprogram.out Restarton-failure WorkingDirectory/home/userabc/progra…

Linux基础练习题1

1、配置网络地址 请为此虚拟机配置以下网络参数&#xff1a; 1&#xff09;主机名&#xff1a;chenyu.example.com &#xff08;将chenyu改成自己名字的全拼&#xff09; 2&#xff09;IP 地址&#xff1a;192.168.100.100/24 3&#xff09;默认网关&#xff1a;192.168.100.25…

# 前端开发规范基础汇总

前端开发规范基础汇总 命名规范 常用的命名规范 camelCase&#xff08;小驼峰式命名法 —— 首字小写&#xff09;PascalCase&#xff08;大驼峰式命名法 —— 首字大写&#xff09;snake_case&#xff08;下划线命名法&#xff09;kebab-case&#xff08;短横线命名法&…

jQuery UI Tabs切换功能实例

jQuery UI Tabs切换功能使用jQuery UI实现Tabs切换功能的方法。代码示例创建了一个包含四个标签页&#xff08;按钮A-D&#xff09;的界面&#xff0c;每个标签对应不同的内容区域。通过引入jQuery UI库并调用tabs()方法实现基本切换功能。文章还提到可以通过配置选项修改默认行…

关于为什么stm32的开漏输出可以读取引脚的数值

在使用软件模拟iic通信时&#xff0c;要将SDA线配置为开漏输出&#xff0c;既然配置为开漏输出&#xff0c;为什么程序还可以通过SDA线读取数据&#xff1f;查阅手册&#xff1a;只说了结论&#xff1a;在开楼模式下&#xff0c;对输入数据寄存器的读访问可以得到IO状态来看输出…

墨者:SQL手工注入漏洞测试(SQLite数据库)

1. 墨者学院&#xff1a;SQL手工注入漏洞测试(SQLite数据库)&#x1f680; 2. SQLite数据库注入特点&#x1f50d; SQLite数据库和MySQL数据库语法不同&#xff0c;不能直接套用MySQL的注入方式。但SQLite有个特殊的数据库sqlite_master&#xff0c;它存储了所有表结构信息&…

【Apache Tomcat】

目录Tomcat 基本简介Tomcat 架构组成Tomcat 的目录结构Tomcat 的工作原理Tomcat 的配置文件Tomcat 与其他服务器对比Tomcat 使用场景Tomcat 与 Spring Boot常见问题与优化Tomcat&#xff08;全称 Apache Tomcat&#xff09;是由 Apache 软件基金会开发和维护的一款 开源的 Web …

Nginx参数proxy_set_header 与 add_header 核心区别

proxy_set_header 与 add_header 是 Nginx 中两个用于操作 HTTP 头部信息的指令&#xff0c;但作用方向和使用场景完全不同。以下是两者的核心区别&#xff1a;核心区别概述特性proxy_set_headeradd_header作用方向✅ 请求头&#xff08;Request Headers&#xff09; → 后端服…

若依框架-前端二次开发快速入门简述

1.目录如左图所示&#xff0c;主要分为bin,build,node_modules,public,src几个部分&#xff0c;我们从gitee上使用bash将项目克隆到本地后&#xff0c;进入项目目录&#xff0c;并安装好依赖后可以直接使用命令启动服务&#xff0c;具体命令见README.md&#xff0c;安装好依赖后…

day 41 类和方法

day 28 类是对属性和方法的封装&#xff0c;可以理解为模版&#xff0c;通过对模型实例化可以实现调用这个类的属性和方法。比如创建一个随机森林类&#xff0c;然后就可以调用它的训练和预测方法。 一个常见的类的定义包括了&#xff1a; 1、关键字class 2、类名 3、语法固定…

Docker学习日志-Docker容器配置、Nginx 配置与文件映射

Docker学习日志-Docker容器配置、Nginx 配置与文件映射 docker run 之后能否再次修改卷映射或端口映射&#xff1f; 不能直接修改已创建容器的卷映射或端口映射。 Docker 的设计原则是 **容器是不可变的 **&#xff0c;也就是说&#xff1a; 一旦容器通过 docker run 创建完成&…