【优选算法】—— 双指针问题

news2024/12/26 3:31:45

从今天开始,整个暑假期间。我将不定期给大家带来有关各种算法的题目,帮助大家攻克面试过程中可能会遇到的算法这一道难关。


目录

(一) 基本概念

(二)题目讲解

1、难度:easy

1️⃣移动零

2️⃣复写零

2、难度:medium

1️⃣快乐数

2️⃣盛⽔最多的容器

3、难度:difficult

2️⃣最大得分

总结


(一) 基本概念

双指针算法是一种常用的算法技巧,它通常用于在数组或字符串中进行快速查找、匹配、排序或移动操作。双指针算法使用两个指针在数据结构上进行迭代,并根据问题的要求移动这些指针。

常⻅的双指针有两种形式:

  • ⼀种是对撞指针
  • ⼀种是左右指针
     

对撞指针:⼀般⽤于顺序结构中,也称左右指针。
• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
• 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
          ◦ left == right (两个指针指向同⼀个位置)
          ◦ left > right (两个指针错开)



快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动


这种⽅法对于处理环形链表或数组⾮常有⽤。


其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。


快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
        • 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢
 

【优势】 

  1. 双指针算法的优势在于它们能够在一次迭代中完成操作,时间复杂度通常比较低,并且不需要额外的空间;
  2. 通过合理地移动指针,可以有效地减少不必要的计算和比较,提高算法的效率。

在具体应用双指针算法时,需要根据问题的特点和要求选择合适的指针移动策略,确保算法的正确性和高效性。同时,注意处理边界条件和特殊情况,以避免错误和异常。


(二)题目讲解

接下来,我们通过几道题目让大家具体的感受一下。(题目由易到难

1、难度:easy

1️⃣移动零

链接如下:283. 移动零
【题⽬描述】

 【解法】(快排的思想:数组划分区间-数组分两块

算法思路:

  1. 在本题中,我们可以⽤⼀个 cur 指针来扫描整个数组,另⼀个 dest 指针⽤来记录⾮零数序列的最后⼀个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
  2. 在 cur 遍历期间,使 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零

【算法流程】

a. 初始化 cur = 0 (⽤来遍历数组),dest = -1 (指向⾮零元素序列的最后⼀个位置。
因为刚开始我们不知道最后⼀个⾮零元素在什么位置,因此初始化为  -1 )


b. cur 依次往后遍历每个元素,遍历到的元素会有下⾯两种情况:


    i. 遇到的元素是 0 , cur 直接 ++ 。因为我们的⽬标是让 [dest + 1, cur - 1] 内
的元素全都是零,因此当 cur 遇到 0 的时候,直接 ++ ,就可以让 0 在 cur - 1
的位置上,从⽽在 [dest + 1, cur - 1] 内;
    ii. 遇到的元素不是 0 , dest++ ,并且交换 cur 位置和 dest 位置的元素,之后让cur++ ,扫描下⼀个元素。

  •  因为 dest 指向的位置是⾮零元素区间的最后⼀个位置,如果扫描到⼀个新的⾮零元素,那么它的位置应该在 dest + 1 的位置上,因此 dest 先⾃增 1 ;
  •  dest++ 之后,指向的元素就是 0 元素(因为⾮零元素区间末尾的后⼀个元素就是0 ),因此可以交换到 cur 所处的位置上,实现 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零。

【算法实现】

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        for(int cur = 0, dest = -1; cur < nums.size(); cur++)
        if(nums[cur]) // 处理⾮零元素
            swap(nums[++dest], nums[cur]);
    }
};

【结果展示】

 【性能分析】

具体的性能分析如下:

  • 时间复杂度:代码中使用了一个循环来遍历数组,因此时间复杂度为 O(n),其中 n 是数组的长度。
  • 空间复杂度:代码中没有使用额外的空间,只是通过交换数组元素的方式来实现移动,所以空间复杂度为 O(1)

该算法的性能较好,处理速度快,且空间开销较小。由于只进行一次遍历,并且只进行元素交换操作,因此在大多数情况下,时间复杂度为线性级别。然而,在某些特殊情况下,比如数组中几乎所有元素都是非零元素时,仍需要遍历整个数组,但交换操作的次数会减少。

 


2️⃣复写零
 

链接如下:1089. 复写零

【题⽬描述】

 

【解法】(原地复写---双指针

  • 算法思路:

① 如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。


② 但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两步:

  • 先找到最后⼀个复写的数;
  • 然后从后向前进⾏复写操作
     

 

【算法流程】

a. 初始化两个指针 cur = 0 , dest = 0 ;


b. 找到最后⼀个复写的数:
   i. 当 cur < n 的时候,⼀直执⾏下⾯循环:
        • 判断 cur 位置的元素:
                ◦ 如果是 0 的话, dest 往后移动两位;
                ◦ 否则, dest 往后移动⼀位。
        • 判断 dest 时候已经到结束位置,如果结束就终⽌循环;
        • 如果没有结束, cur++ ,继续判断。


c. 判断 dest 是否越界到 n 的位置:
        i. 如果越界,执⾏下⾯三步:
                1. n - 1 位置的值修改成 0 ;
                2. cur 向移动⼀步;
                3. dest 向前移动两步。


d. 从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:
        i. 判断 cur 位置的值:
                1. 如果是 0 : dest 以及 dest - 1 位置修改成 0 , dest -= 2 ;
                2. 如果⾮零: dest 位置修改成 0 , dest -= 1 ;
        ii. cur-- ,复写下⼀个位置

【算法实现】

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        // 1. 先找到最后⼀个数
        int cur = 0, dest = -1, n = arr.size();
        while(cur < n)
        {
            if(arr[cur]) dest++;
            else dest += 2;
            if(dest >= n - 1) break;
            cur++;
        }
        // 2. 处理⼀下边界情况
        if(dest == n)
        {
            arr[n - 1] = 0;
            cur--; dest -=2;
        }
        // 3. 从后向前完成复写操作
        while(cur >= 0)
        {
            if(arr[cur]) arr[dest--] = arr[cur--];
            else
            {
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
        }
    }
};

【结果展示】

 

【性能分析】

具体的性能分析如下:

  • 时间复杂度:代码中使用了两个循环,第一个循环用于找到最后一个数、处理边界情况,第二个循环用于从后向前完成复写操作。由于第二个循环是针对数组长度的常数倍进行的操作,因此时间复杂度为 O(n),其中 n 是数组的长度。
  • 空间复杂度:代码中没有使用额外的空间,只是通过修改原数组实现复写操作,所以空间复杂度为 O(1)

该算法的性能较好,时间复杂度为线性级别,空间开销较小。在最坏情况下,需要遍历整个数组进行复写操作,但仍保持了线性时间复杂度。


2、难度:medium


1️⃣快乐数
 

链接如下:202. 快乐数

【题⽬描述】

 

【解法】(快慢指针)

使用双指针的算法判断快乐数的基本思路如下:

  1. 定义两个指针,一个指针快指针(fast)每次向前移动两步,一个慢指针(slow)每次向前移动一步。
  2. 将给定的正整数转换为字符串形式。
  3. 在一个循环中,不断计算当前数字的各个位上的数字的平方和。
  4. 将得到的平方和作为下一个数字,继续计算平方和,直到平方和等于 1,或者出现循环。
  5. 判断循环是否发生,如果发生循环并且平方和不等于 1,说明该数字不是快乐数。

【算法流程】

a. 初始化快慢指针为给定的正整数。

b. 在一个循环中,计算快慢指针指向的数字的各个位上的数字的平方和,并将结果赋值给快指针。

c. 同时,慢指针移动一步。

d. 检查快指针和慢指针是否指向同一个数字,如果是,则说明存在循环,退出循环。

e. 如果快指针指向的数字等于 1,则说明是快乐数,返回 true。

f. 否则,将慢指针指向的数字赋值给快指针,重复步骤 b-e。

g. 如果循环结束仍未找到快乐数,则返回 false。

【算法实现】

class Solution {
public:
    int getNext(int n) {
    int sum = 0;
    while (n > 0) {
        int digit = n % 10;
        sum += digit * digit;
        n /= 10;
    }
    return sum;
}

    bool isHappy(int n) {
        int slow = n;
        int fast = getNext(n);

        while (fast != 1 && slow != fast) {
            slow = getNext(slow);
            fast = getNext(getNext(fast));
        }

        return fast == 1;
    }
    
};

【结果展示】

 

【性能分析】

具体的性能分析如下:

  1. 时间复杂度:

    • 在计算下一个数字的过程中,需要遍历每个数字的位数,因此时间复杂度为 O(logn),其中 n 是给定的数字。
    • 快慢指针在循环中遍历数字,直到找到结果或者出现循环。最坏情况下,循环次数是一个数字中各个位数的总和,也就是 O(logn)。
    • 因此,总体时间复杂度为 O(logn)
  2. 空间复杂度:只使用了常量级别的额外空间,不随输入规模变化,因此空间复杂度为 O(1)


 

2️⃣盛⽔最多的容器
 

链接如下:11. 盛最多水的容器

【题⽬描述】

 

【解法】(对撞指针)

  • 首先,初始时左指针指向数组的起始位置,右指针指向数组的结束位置。计算当前区间内的容器的盛水量,并更新最大盛水量。
  • 接着,判断两个指针所指向的元素的高度,将较小的元素的指针向内移动一步。这样可以保证每次移动都在选择更高的边界线,以获得可能的更大盛水量。
  • 重复上述步骤,直到两个指针相遇,即左指针大于等于右指针。此时遍历完成,返回最大盛水量。

【算法流程】

a.初始化最大盛水量 maxArea 为 0,左指针 left 指向数组起始位置,右指针 right 指向数组结束位置。

b.进入循环,判断条件为左指针小于右指针。

       ◦   计算当前区间的宽度、最小高度以及当前盛水量。

       ◦   更新最大盛水量,取当前盛水量与历史最大盛水量的较大值。

       ◦   判断两个指针所指向元素的高度,将较小元素的指针向内移动一步。

c. 循环结束后,返回最大盛水量作为结果。

【算法实现】

class Solution {
public:
    int maxArea(vector<int>& height) {
        int res = 0;
        int left = 0;
        int right = height.size() - 1;

        while (left < right) {
            int width = right - left;
            int minHeight = min(height[left], height[right]);
            int v = width * minHeight;
            res = max(res, v);

            if (height[left] < height[right]) {
                left++;
            } 
            else {
                right--;
            }
        }

        return res;
    }
};

【结果展示】

 

【性能分析】

具体的性能分析如下:

  • 时间复杂度分析:对撞指针移动的过程中,每次都排除了一部分区域,因此时间复杂度为 O(n),其中 n 是数组的长度。
  • 空间复杂度分析:该算法只使用了常数级别的额外空间,存储了少量的变量和指针。因此,空间复杂度为 O(1),与输入规模无关。

3、难度:difficult

2️⃣最大得分

链接如下:1537. 最大得分

【题⽬描述】

 

【算法流程】

  1. 初始化双指针 ij 分别为 0,并初始化两个变量 sum1sum2 用于记录当前路径的和,初始化 res 用于记录最大得分。

  2. 在循环中,比较当前指针位置上 nums1[i]nums2[j] 的值:

    • 如果 nums1[i] < nums2[j],则将 nums1[i] 加到 sum1 中,并将指针 i 向后移动一位。
    • 如果 nums1[i] > nums2[j],则将 nums2[j] 加到 sum2 中,并将指针 j 向后移动一位。
    • 如果 nums1[i] 和 nums2[j] 相等,意味着遇到了相同的值,此时需要考虑路径的切换。比较 sum1 和 sum2 的大小,将较大的值加到 res 中,并将 sum1 和 sum2 清零。然后将 nums1[i] 加到 res中,同时将指针 i 和 j 向后移动一位。
  3. 循环结束后,可能还会存在剩余未遍历的元素。此时,需要将剩余路径的和加到 res中。

  4. 返回 res % num,即对 10^9 + 7 取余后的最大得分。

 

【算法实现】

class Solution {
public:
    const int num = 1e9 + 7;

    int maxSum(vector<int>& nums1, vector<int>& nums2) {
        int n1 = nums1.size(), n2 = nums2.size();
        int i = 0, j = 0;
        long long sum1 = 0, sum2 = 0;  // 使用 long long 类型防止整数溢出
        long long res = 0;

        while (i < n1 && j < n2) {
            if (nums1[i] < nums2[j]) {
                sum1 += nums1[i++];
            } 
            else if (nums1[i] > nums2[j]) {
                sum2 += nums2[j++];
            } 
            else {                     // 遇到相同值,取两个路径中的较大值
                res += max(sum1, sum2) + nums1[i];
                sum1 = 0, sum2 = 0;
                
                i++;
                j++;
            }
        }

        // 处理剩余的元素
        while (i < n1) {
            sum1 += nums1[i++];
        }
        while (j < n2) {
            sum2 += nums2[j++];
        }

        res += max(sum1, sum2);  // 加上剩余路径的和

        return res % num;
    }
};

【结果展示】

 

 【性能分析】

具体的性能分析如下:

  • 时间复杂度分析: O(n1 + n2),其中 n1 和 n2 分别是两个输入数组的长度。因为我们同时遍历了两个数组,所以时间复杂度与两个数组的总长度成线性关系。
  • 空间复杂度分析: O(1),只使用了几个变量来保存结果,所以额外空间是固定的,不会随输入规模增加而增加。

总结

以上便是本期关于双指针算法的全部讲解内容。如果大家掌握了上述知识,再去勤加练习的话我相信以后在遇到此类问题都可迎刃而解。

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

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

相关文章

数据结构——二叉树的实现

什么是二叉树&#xff1f; 二叉树&#xff08;binary tree&#xff09;是指树中节点的度不大于2的有序树&#xff0c;它是一种最简单且最重要的树。二叉树的递归定义为&#xff1a;二叉树是一棵空树&#xff0c;或者是一棵由一个根节点和两棵互不相交的&#xff0c;分别称作根…

【实验七】界面化和事件处理

1、完成书上239页实验题目1和题目2 题目1 package chap10.test; //awt是一个软件包&#xff0c;包含用于创建用户界面和绘制图形图像的所有分类。 import java.awt.BorderLayout;//边界管理器import java.awt.Color; import java.awt.FlowLayout;//流布局一般用来安排面板中的…

Django_自定义文件存储类并将图片上传到FastDFS

目录 将图片到FastDFS和浏览的流程图 使用自定义文件存储类 1、定义存储类 2、创建FastDFS的配置文件 3、修改settings.py配置 4、上传图片进行验证 4.1 定义一个包含ImageField字段的模型类 4.2 登录django的admin后台 4.3 上传图片 4.4 查看图片 源码等资料获取方…

What Is the Character Limit for ChatGPT? 聊天GPT的字符限制是多少?

The character limit for ChatGPT text prompts is 4096 characters, and there’s also a limit of 4096 tokens per conversation. If you aren’t sure how many tokens your prompt uses, you can calculate that with OpenAI’s Tokenizer tool. ChatGPT 文本提示的字符限…

AutoHand第三人称的AutoHandVRIK开启和关闭

第三人称结构 第三人称在AutoHandVRIK脚本初始化完毕后&#xff0c;会出把两只手提到玩家对象根部对象上&#xff0c;并且在原始的模型手的位置创建了新的对象&#xff08;leftHandFollowTarget&#xff0c;rightHandFollowTarget&#xff09;&#xff0c;副VRIK大概用途就是I…

从字母百分比看字符串处理的奥妙

本篇博客会讲解力扣“2278. 字母在字符串中的百分比”的解题思路&#xff0c;这是题目链接。 本题的思路是&#xff0c;统计字符letter在字符串s中出现的次数cnt&#xff0c;以及字符串s的长度n&#xff0c;其中n也可以使用strlen来求。最终要求计算的百分比是cnt*100/n。 int…

性能测试工具LoadRunne函数如何使用详解(上)

性能测试工具LoadRunner是软件测试行业主流测试工具&#xff0c;也是软件测试实验室在申请CNAS/CMA资质、测试质量体系建设一定会用到的一款软件测试工具。LoadRunner的函数是是组成测试脚本的最小单元&#xff0c;它真实模拟 一个个请求操作&#xff0c;并接收返回结果。学习L…

王道考研数据结构——顺序表

08 线性表的基本概念 对参数的修改结果需要“带回来”就使用引用类型的参数 顺序表的定义&#xff1a; 顺序存储、链式存储 sizeof(int)、sizeof(MyStruct) 顺序表的扩展 虽然大小改变&#xff0c;但是时间开销大 让代码健壮&#xff1a;对数据操作的合法性进行检查&#xf…

How to Write and Publish a Scientific Paper-How to Prepare Effective Graphs

How to Prepare Effective Graphs WHEN NOT TO USE GRAPHS 在上一章中&#xff0c;我们讨论了某些不应该制成表格的数据类型。它们也不应该被转换成图表。基本上&#xff0c;图表都是图形表格。 要点是这样的。某些类型的数据&#xff0c;特别是稀疏类型或单调重复的类型&am…

细节决定成败,聊聊JS的类型(下)

讲完了基本类型&#xff0c;我们来介绍一个现象&#xff1a;类型转换。 因为 JS 是弱类型语言&#xff0c;所以类型转换发生非常频繁&#xff0c;大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉&#xff0c;但是如果我们不去理解类型转换的严格定义&a…

牛客练习赛113

A小红的基环树 题目描述 定义基环树为n个节点、n条边的、没有自环和重边的无向连通图。 定义一个图的直径是任意两点最短路的最大值。 小红想知道&#xff0c;n个节点构成的所有基环树中&#xff0c;最小的直径是多少&#xff1f; 思路分析 特判n3时为1&#xff0c;其他时候都…

行业追踪,2023-07-11,新增加 rps50 排名,汽车零部件回落 10 日均线,直接反弹

自动复盘 2023-07-11 成交额超过 100 亿 排名靠前&#xff0c;macd柱由绿转红 成交量要大于均线 有必要给每个行业加一个上级的归类&#xff0c;这样更能体现主流方向 rps 有时候比较滞后&#xff0c;但不少是欲杨先抑&#xff0c; 应该持续跟踪&#xff0c;等 macd 反转时参与…

Stable Diffusion - 提示词翻译插件 sd-webui-prompt-all-in-one

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/131649921 sd-webui-prompt-all-in-one 是一个基于 stable-diffusion-webui 的扩展&#xff0c;目的是提高 提示词/反向提示词 输入框的使用体验&…

汽车应用级MS35774/MS35774A低噪声 256 细分微步进电机驱动

MS35774/MS35774A 是一款高精度、低噪声的两相步进 电机驱动芯片&#xff0c;芯片内置功率MOSFET &#xff0c;长时间工作的平均电 流可以达到 1.4A &#xff0c;峰值电流 2A 。芯片集成了过温保护、欠压 保护、过流保护、短地保护、短电源保护功能。 主要特点 ◼ 2 相步进…

【python】Excel文件的插入操作

上篇文章写了pandas读取Excel文件的操作&#xff0c;但实际应用中&#xff0c;我们还需要对Excel指定单元格进行写入操作。 pandas模块写入 pandas模块方法写入&#xff0c;简单粗暴&#xff0c;就是把整个Excel文件重写 import pandas as pdfile_path 测试用例.xlsx # 读取Ex…

【Java基础】符号位、原码、补码、反码、位逻辑运算符、位移运算符、复合位赋值运算符

文章目录 前言&#xff1a;符号位、原码、补码、反码1.是什么2.各种码转换 一.二进制高低位1.1.什么是高低位1.2.高低位交换 二.位逻辑运算符位与运算 &位或运算 |异或运算 ^取反运算 ~ 三.位移运算符左位移运算符 <<右位移运算符 >>1.正数右移&#xff1a;2.负…

什么是QPS,什么是RT

&#x1f3c6;今日学习目标&#xff1a; &#x1f340;什么是QPS&#xff0c;什么是RT ✅创作者&#xff1a;林在闪闪发光 ⏰预计时间&#xff1a;30分钟 &#x1f389;个人主页&#xff1a;林在闪闪发光的个人主页 &#x1f341;林在闪闪发光的个人社区&#xff0c;欢迎你的加…

生产慢查询问题分析

1.问题描述 7月1日零点set_24出现大量慢查询告警&#xff0c;经DBA定位为子系统涉及的一条查询语句出现慢查询&#xff0c;引起set_24的cpu使用率突增&#xff0c;触发大量告警&#xff0c;查看生产执行计划发现慢查询为索引跳变引起&#xff1b;具体出现问题的sql语句如下&am…

【裸机开发】I2C 通信接口(三)—— I2C 底层驱动接口实现

目录 一、I2C 初始化 二、产生开始 / 停止信号 1、开始信号 2、重复开始信号 3、停止信号 三、向总线上发送数据&#xff08;阻塞模式&#xff09; 四、从总线上读取数据&#xff08;阻塞模式&#xff09; 五、整合&#xff1a;数据读写统一调用接口 一、I2C 初始化 初…

springboot驾校管理系统

框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 …