双指针算法专题(1)

news2024/11/18 1:27:24

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏: 优选算法专题

目录

双指针算法的介绍 

283. 移动零

1089. 复写零

202. 快乐数

11.盛最多水的容器


双指针算法的介绍 

在正式做题之前,得先了解:什么是双指针? 

双指针一般是指两个不同的变量,它们分别指向数据的不同位置。这两个指针可以根据特定的问题需求以不同的方式移动和操作,从而实现高效的算法和数据处理。

先来了解一个最简单的双指针。

上面这种事最简单的方法,但是其有一个缺点:需要另外申请一份内存空间,也就是只能 “异地” 操作,如果想要在原地修改就做不到,因此,这里就引出了我们的双指针算法。

这里我们就可以得出一个结论:双指针算法可以将 “异地” 操作,转变为 “原地” 操作。

常见的双指针有两种:一种是对撞指针,也称为左右指针;另一种是快慢指针。

什么是对撞指针呢?对撞指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近。对撞指针的终止条件一般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:left == right(两个指针指向同一个位置)  left > right(两个指针错开)。其实就是 当 left >= right 时,循环就可以停止了。最常见的对撞指针就是我们前面学习的快速排序和二分查找算法。

什么是快慢指针呢?其又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。这种方法对于处理环形链表或数组非常有用。其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想。
快慢指针的实现方式有很多种,最常用的一种就是:在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢。我们前面在原地删除值为 val 的元素的方法就类似于快慢指针,只不过我们没有设置指针的速度而已。

双指针算法对于数组分块的问题的处理是非常有效的。

下面我们就来实战一些题目: 

283. 移动零

题目:

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

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

示例 2:

输入: nums = [0]输出: [0]

提示:

  • 1 <= nums.length <= 104
  • -231 <= nums[i] <= 231 - 1

思路: 

题目已经明确地告诉我们了,不能申请一个新的数组,只能原地进行修改。在做题之前,我们最好能画图。通过画图来揣摩出该用哪种算法来解决。

上面这种采用的是对撞指针,但很明显不行,因此我们就只能采用快慢指针。

理想是丰满的,现实是骨感的。上述情况虽然在画图时确实好解决,但是在实际编码时,会出现很多种不确定的情况。因此优化成这样:fast找非零元素,直接和slow进行交换。

有的小伙伴这里可能会有疑惑:slow万一也是非零数呢?它们一交换,不就会影响非零数的相对顺序吗?其实这种情况是不存在的,假设两种极端情况:

1、数组中全部是非零数,那么全部就只是自己和自己进行交换;

2、数组中全部是零,这就更不可能了,if 语句的进不去,咋交换呢? 

代码实现:

class Solution {
    public void moveZeroes(int[] nums) {
        for (int fast = 0, slow = 0; fast < nums.length; fast++) {
            // fast走到非零位置就进行交换,走到零位置什么也不处理,让其++即可
            if (nums[fast] != 0) {
                int temp = nums[fast];
                nums[fast] = nums[slow];
                nums[slow] = temp;
                slow++;
            }
        }
    }
}

总结:一次性做不到完美没关系,多试几次就好啦!算法就是在不断的犯错中学习进步的。 

1089. 复写零

题目:

给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。

注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。

示例 1:

输入:arr = [1,0,2,3,0,4,5,0]
输出:[1,0,0,2,3,0,0,4]
解释:调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]

示例 2:

输入:arr = [1,2,3]
输出:[1,2,3]
解释:调用函数后,输入的数组将被修改为:[1,2,3]

提示:

  • 1 <= arr.length <= 104
  • 0 <= arr[i] <= 9

思路:首先,想要的就是去遍历这个数组,遇到0就复写,否则就不作处理。如果是在另外一个数组上根据上面的做法可行,但是在一个数组里面的话,就会出现覆盖的情况。

原地修改的话,就是去遍历数组,但是直接从头开始遍历的话,肯定是不行的,因为会出现覆盖的情况,那我们就尝试着从后开始遍历。但问题来了:从那个位置开始呢?因为复写的问题,导致我们不能够正确的找到最后一个元素的位置,因此这里就得解决找到最后一个元素的问题。

找到最后一个元素之后,就从该位置开始往前遍历去往原数组中进行复写操作。

代码实现:

错误版本:

class Solution {
    public void duplicateZeros(int[] arr) {
        // 开始找最后一个位置
        int count = 0;
        int cur = 0;
        for (; cur < arr.length; cur++) {
            if (arr[cur] != 0) {
                count++;
            } else {
                count += 2;
            }
            if (count >= arr.length) { // 一次走两步,可能出现 > 的情况
                // 说明此时已经找到最后一个元素了
                break;
            }
        }
        int dest = arr.length-1;
        while (cur >= 0 && dest >= 0) {
            if (arr[cur] != 0) {
                // 复写一次
                arr[dest--] = arr[cur--];
            } else {
                // 得复写两次
                cur--;
                arr[dest--] = 0;
                if (dest < 0) { // 这里可能会出现越界的情况
                    break;
                }
                arr[dest--] = 0;
            }
        }
    }
}

上面的代码在大多数测试用例下都能正常通过,但是有一个特殊的测试用例要注意:

在这个测试用例中 cur 指向数组下标为5的地方,后续在进行复写的时候,会出现一种情况:dest 的位置出现在了 cur 的前方,也就导致了最后复写的结果全为0了。也就是 dest 指针走到 cur 指针的前方,覆盖了原来的值,致使出错。那为什么会出现这种情况呢?很简单,只有 dest 一次 走两步才会超过 cur 的位置,因此我们可以设置一下让 dest 最开始的位置只走一步,也就是值复写一次0即可。那么问题又来了:什么时候让 dest 在最开始的位置只走一步呢?仔细观察一下:是不是刚刚的这种情况是 count > arr.length 才出现的,而 count == arr.length 的时候是正常进行的。因此当 count > arr.length 时,就让两者都只走一步。其实这种情况(count > arr.length)也就是因为数组空间不足,不能够将那个多余的0给存起来,就导致了覆盖。因此我们也只是让这个dest 值走一步,另一步我们认为其在不存在的地方走完了即可。

正确版本:

class Solution {
    public void duplicateZeros(int[] arr) {
        // 开始找最后一个位置
        int count = 0;
        int cur = 0;
        for (; cur < arr.length; cur++) {
            if (arr[cur] != 0) {
                count++;
            } else {
                count += 2;
            }
            if (count >= arr.length) { // 一次走两步,可能出现 > 的情况
                // 说明此时已经找到最后一个元素了
                break;
            }
        }
        int dest = arr.length-1;
        if (count > arr.length) {
            // 减少 dest 移动的次数(和 cur 一样暂时只移动一次)
            arr[dest--] = arr[cur--];
        }
        while (cur >= 0 && dest >= 0) {
            if (arr[cur] != 0) {
                // 复写一次
                arr[dest--] = arr[cur--];
            } else {
                // 得复写两次
                cur--;
                arr[dest--] = 0;
                if (dest < 0) { // 这里可能会出现越界的情况
                    break;
                }
                arr[dest--] = 0;
            }
        }
    }
}

202. 快乐数

题目:

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

示例 1:

输入:n = 19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

示例 2:

输入:n = 2
输出:false

提示:

  • 1 <= n <= 2^31 - 1

思路: 根据快乐数的定义,我们可以知道:在进行转换过程中只会出现两种情况:1、无限循环但结果并不为1;2、在转换过程中出现结果为1。其实第二种情况也是无限循环,只不过循环的结果一直是1而已。

因此可以总结出下面的规律:

这里的思路也就出来了:通过快慢指针找到环, 接着判断环的元素值是否为1即可。

代码实现:

class Solution {
    public boolean isHappy(int n) {
        // 通过快慢指针来进行判断环值是否为1
        int fast = n;
        int slow = n;
        // 注意得让它们先走,否则两者就还是相等的
        slow = waterFlower(slow);
        fast = waterFlower(fast);
        fast = waterFlower(fast);
        while (slow != fast) {
            // slow一次走一步,fast一次走两步
            slow = waterFlower(slow);
            fast = waterFlower(fast);
            fast = waterFlower(fast);
        }
        return slow == 1; // 看相遇的值是否为1,是1就是快乐树,否则就不是快乐数
    }

    private int waterFlower(int n) {
        // 先计算出n的位数
        int temp = n;
        int count = 0;
        while (temp != 0) {
            count++;
            temp /= 10;
        }
        int sum = 0;
        // 通过位数确定循环的次数
        for (; count != 0; count--) {
            sum += (int) Math.pow((n%10),2);
            n /= 10;
        }
        return sum;
    }
}

注意:这里去转换的方法是模拟 计算水仙花数 的方法写的,我们也可以直接通过计算每一位的值平方,然后再相加也可以。 

11.盛最多水的容器

题目: 

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

输入:height = [1,1]
输出:1

提示:

  • n == height.length
  • 2 <= n <= 105
  • 0 <= height[i] <= 104

思路:题目是让我们求最大盛水量,其实也就是最大的容积。对于这个的话,直接遍历去找最大容积即可。容积公式 = 容器之间的长度 * 容器的最小高度(容器可能不是高度相等的)。

代码实现:

错误版本:直接暴力枚举,双层循环遍历。

class Solution {
    错误解法:暴力枚举
    public int maxArea(int[] height) {     
        int max = 0;
        for (int i = 0; i < height.length; i++) {
            for (int j = i+1; j < height.length; j++) {
                // 计算最大值并更新
                int temp = (j-i) * (height[j] > height[i] ? height[i] : height[j]);
                if (temp > max) {
                    max = temp;
                }
            }
        }
        return max;
    }
}

很显然,上面这个代码的时间复杂度达到了O(N^2),会超出时间限制的。但是肯定是使用这种方式来计算的,只不过我们的时间复杂度过大,因此这里就得找到减少遍历次数的问题。我们可以尝试从数组的两头开始遍历,降低时间复杂度。

注意:

1、这里之所以不让left 和 right 同时往"后"遍历,是因为如果同时遍历的话,可能会漏掉最大值的情况(会有情况被漏掉)。 

2、每一次只移动最小的指针值,就保证了下一次的容积可能不会小于这次的。但如果移动最大值的话,容积就肯定小于这次了。因为 最小值没有变(容器高度),然后 两者之间的距离变小了,那么最终的结果也就变小了。

正确版本:使用对撞指针减少遍历的次数。

class Solution {
    public int maxArea(int[] height) {
        int left = 0;
        int right = height.length-1;
        int max = 0;
        while (left < right) {
            int min = height[left] < height[right] ? height[left] : height[right];
            int temp = (right-left) * min;
            if (temp > max) {
                max = temp;
            }
            if (min == height[left]) {
                left++;
            } else {
                right--;
            }
        }
        return max;
    }
}

好啦!本期 双指针算法专题(1)的学习之旅就到此结束啦!我们下一期再一起学习吧!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 获取文件夹下的全部文件及指定文件(代码)

文章目录 1.&#xff08;C17&#xff09;获得指定目录下的所有文件&#xff08;不搜索子文件夹&#xff09;2.&#xff08;C11&#xff09;获得指定目录下的所有文件&#xff08;不搜索子文件夹&#xff09;3.&#xff08;C11&#xff09;获取目录下指定格式的所有文件&#xf…

工厂安灯系统在优化生产流程上的优势

工厂安灯系统通过可视化的方式&#xff0c;帮助工厂管理者和操作工人及时了解生产状态&#xff0c;快速响应问题&#xff0c;从而优化生产流程。 一、安灯系统实时监控与反馈 安灯系统的核心功能是实时监控生产线的状态。通过在生产现场设置灯光、显示屏等设备&#xff0c;工人…

OpenGL(四) 纹理贴图

几何模型&材质&纹理 渲染一个物体需要&#xff1a; 几何模型&#xff1a;决定了物体的形状材质&#xff1a;绝对了当灯光照到上面时的作用效果纹理&#xff1a;决定了物体的外观 纹理对象 纹理有2D的&#xff0c;有3D的。2D图像就是一张图片&#xff0c;3D图像是在…

记得忘记密码情况下如何退出苹果Apple ID

在日常使用苹果手机时&#xff0c;我们可能会遇到需要退出Apple ID的情况&#xff0c;比如更换手机、不再使用某些服务或出于安全考虑等。下面&#xff0c;我们就来详细介绍一下苹果手机如何退出Apple ID。 情况一&#xff1a;记得Apple ID密码 若是记得Apple ID密码&#xff…

3. 轴指令(omron 机器自动化控制器)——>MC_HomeWithParameterMC_Move

机器自动化控制器——第三章 轴指令 3 MC_HomeWithParameter变量▶输入变量▶输出变量 功能说明▶原点复位动作与原点复位参数的关系▶重启运动指令▶多重启动运动指令▶错误代码 MC_Move变量▶输入变量▶输出变量▶输入输出变量 功能说明▶指令详情▶重启运动指令▶多重启动运…

pytorch入门(1)——pytorch加载数据初认识

环境配置及其安装&#xff1a; 2023最新pytorch安装&#xff08;超详细版&#xff09;-CSDN博客 pytorch加载数据初认识 Dataset&#xff1a;创建可被Pytorch使用的数据集 提供一种方式获取数据及其label Dataloader&#xff1a;向模型传递数据 为网络提供不同的数据形式 …

算法_队列+宽度优先搜索---持续更新

文章目录 前言N叉树的层序遍历题目要求题目解析代码如下 二叉树最大宽度题目要求题目解析代码如下 在每个树中找最大值题目要求题目解析代码如下 二叉树的锯齿形层序遍历题目要求题目解析代码如下 前言 本文将会向你介绍有关队列宽度优先搜索的题目&#xff1a;N叉树的层序遍历…

【每日一题】LeetCode 2398.预算内的最多机器人数目(滑动窗口、数组、二分查找、前缀和、堆(优先队列))

【每日一题】LeetCode 2398.预算内的最多机器人数目&#xff08;滑动窗口、数组、二分查找、前缀和、堆&#xff08;优先队列&#xff09;&#xff09; 题目描述 给定两个整数数组 chargeTimes 和 runningCosts&#xff0c;分别代表 n 个机器人的充电时间和运行成本。再给定一…

喂料机和失重秤的区别?

喂料机和失重秤的区别&#xff1f;在硬件结构上的具体差异&#xff1a; 1. 喂料机的硬件结构 喂料机的结构比较简单&#xff0c;主要功能是传送物料&#xff0c;不涉及精确的称重系统。其硬件结构通常包括以下部分&#xff1a; 料斗&#xff1a;用于存储物料&#xff0c;物料…

cesium.js 入门到精通(7)

我们说一下相机的概念&#xff1a; 生活中的相机是一个用来拍照的设备&#xff0c;而这里的相机应该理解成一个人机交互的媒介。地图的缩放、平移、旋转&#xff0c;以及相关的鼠标操作都是由相机作为媒介来实现的。相机的位置和姿态参数决定了我们能看到的地图的样子。 可以…

centos7.9安装clamav教程

本章教程主要记录在centos7.9安装clamav过程。 ClamAV(Clam AntiVirus)是一个开源的防病毒软件工具,主要用于检测和消除恶意软件。它最初由 Tomasz Kojm 于 2001 年开发,并由 Cisco Systems 维护和支持。ClamAV 广泛应用于邮件网关、文件服务器和其他需要防病毒保护的环境中…

Linux软件包循环依赖解决 彻底删除i386架构 更新软件源

0.问题 之前为了wine和intel核显驱动加了32位的库&#xff0c;现在每次apt upgrade更新都被循环依赖弄得不堪其扰&#xff0c;apt --fix-broken install解决缺失都循环报错&#xff0c;寸步难行&#xff0c;忍无可忍、 而且一看全是i386的依赖&#xff0c;这32位我不用也罢&…

apache文件共享和访问控制

实现apache文件共享 文件共享路径 <Directory "/var/www/html"> #默认发布路径&#xff0c;功能限制 Options Indexes FollowSymLinks #indexes支持文件共享功能 AllowOverride None Require all granted </Directory> 进入到该路径下 cd…

【Java Bean Validation API】Spring3 集成 Bean 参数校验框架

Spring3 集成 Bean 参数校验框架 Java Bean Validation API 1. 依赖 Spring 版本&#xff1a;3.0.5 Java 版本&#xff1a;jdk21 检验框架依赖&#xff08;也可能不需要&#xff0c;在前面 spring 的启动依赖里就有&#xff09;&#xff1a; <!-- 自定义验证注解 -->…

【原创】java+springboot+mysql高校社团网系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

spring内置的

程序里注入了spring内置的线程池&#xff0c;但没有看到线程池相关参数配置&#xff08;corePoolSize maxPoolSize 队列大小&#xff09;&#xff0c;网上查说默认是1个线程&#xff0c;结果和生产实际看到的不一致。 从生产可以看到有8个线程在跑&#xff0c;task-1 task-8&am…

buildroot移植qt报错Info: creating stash file (补充qt添加字库)

移植qt库&#xff0c;编译文件报错Info: creating stash file /home/rbing/QT/uart/.qmake.stash Project ERROR: Unknown module(s) in QT: serialport rbingouc:~/QT/uart$ /home/rbing/linux/tool/buildroot-2022.02.9/output/host/usr/bin/qmake Info: creating stash fil…

PCI Express 体系结构导读摘录(六)

系列文章目录 PCI Express 体系结构导读摘录&#xff08;一&#xff09; PCI Express 体系结构导读摘录&#xff08;二&#xff09; PCI Express 体系结构导读摘录&#xff08;三&#xff09; PCI Express 体系结构导读摘录&#xff08;四&#xff09; PCI Express 体系结构导读…

HarmonyOS开发实战( Beta5.0)画笔调色板案例实践

鸿蒙HarmonyOS开发往期必看&#xff1a; HarmonyOS NEXT应用开发性能实践总结 最新版&#xff01;“非常详细的” 鸿蒙HarmonyOS Next应用开发学习路线&#xff01;&#xff08;从零基础入门到精通&#xff09; 介绍 本示例实现了一个网格渐变的画笔调色板&#xff0c;能够根…

Vector - VT System - 板卡_VT板卡使用介绍_01

总体介绍 在常规的车载网络测试中&#xff0c;除了我们常用的使用VN系列设备进行总线协议测试&#xff0c;大多数公司都会将协议强相关的功能测试放在了功能侧&#xff0c;但是实际上这块对于车载网络测试工程师来说也是需要去了解的&#xff0c;毕竟只有懂协议的人才能更好的测…