『 基础算法题解 』之双指针(上)

news2024/12/28 5:20:04

双指针

文章目录

  • 双指针
    • 移动零
      • 题目解析
      • 算法原理
      • 代码
      • 拓展
    • 复写零
      • 题目解析
      • 算法原理
      • 代码
    • 快乐数
      • 题目解析
      • 算法解析
        • 拓展
      • 代码
    • 盛最多水的容器
      • 题目解析
      • 算法解析
      • 代码
    • 有效的三角形个数
      • 题目解析
      • 算法原理
      • 代码


移动零

题目解析

【题目链接】


算法原理

该种题目可以归为一类题数组分块\数组划分;

该题的大概为给定一个数组,并给定一个对应的规则,使得数组按照规则划分为若干个区间;

在这个题目中,最终的数组会被分为两块,分别为:

!= 0

== 0

而在实际的过程中可以分为三块:

假设有两个指针分别为cur指针与dest指针;

cur指针用来遍历数组;

dest指针为已处理区间内,非0元素与0元素的分界线,即最后一个非0元素的位置;

!= 0

== 0

未处理

从上图中可以看到,三个区间分别为

[0,dest],[dest+1,cur-1],[cur,n-1]

这题的思路即为控制dest指针与cur指针,使得数组在每一次的移动或者操作中都始终都趋于该数组分化;

在对两个指针进行初始化时,由于是上面的三段区间,cur又因为需要用来遍历整个数组,所以初始化cur的位置应该是在数组首元素的位置(即0位置);

而对于dest指针来说,该指针用来划分非0元素与0元素,但是在最初始的情况下并没有非0元素区间,所以该段区间不存在,dest指针最初的位置应该在0位置的前一个,也就是-1的位置;

cur指针遍历到一个0元素时不进行操作,但cur指针遍历到一个非0元素时,应该移动dest指针使其++,并交换dest指针所指向的元素与cur遇到的非0元素;

以第一个测试用例[ 0 , 1 , 0 , 3 , 12 ]

该测试用例的答案为[ 1 , 3 , 12 , 0 , 0 ](分块过后非0元素的顺序不发生改变)


代码

根据上述描述只需要用双指针,利用迭代即可解决;

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int cur = 0;
        int dest = -1;
        while(cur<nums.size()){
            if(nums[cur]){
                swap(nums[cur],nums[++dest]);
            }
            ++cur;
        }
    }
};

拓展

对于移动0这个题目来说,其实它的原理与快速排序的每一趟都是一样的;

快速排序单趟排序hoare原生的思路即为:定义一个基准值,将大于或者小于基准值的数据分别进行分类;




复写零

题目解析

【题目链接】

题目的几个点:

  • 将0复写
  • 不能越界
  • 原地算法

算法原理

该题的解法是用双指针;

而该双指针的解法是由异地算法进行推断的;

以测试用例[ 1 , 0 , 2 , 3 , 0 , 4 , 5 , 0 ]为例;

  • 异地

    使用异地算法即多开一个同样大小的空间,遇到0即复写2次,遇到1即复写一次;

    同时在复写的过程中判断在复写过程中是否会产生越界;

    class Solution {
    public:
        void duplicateZeros(vector<int>& arr) {
            size_t len = arr.size();
            vector<int> tmp(arr.size());
            for(size_t cur = 0, dest = 0;dest<len;++cur,++dest){
                tmp[dest] = arr[cur];
                if(dest+1 <len && arr[cur] == 0){
                    tmp[++dest] = 0;
                }
            }
            arr = tmp;
        }
    };
    
  • 原地

    若是在原地的话则不能使用从左向右遍历,若是从左向右遍历的话将会出现有效内容被覆盖的问题;

如图所示,若是双指针都从前往后进行遍历的话将会出现覆盖的问题;

所以在复写的过程中我们可以推断出应该由后向前进行遍历;

由该测试用例的异地操作可知,最后一个复写的数据为4;下标为5的数据;

假设cur指针指向该数据,而dest指针指向n-1的位置由后向前进行遍历,且复写的规则不变;

从上述操作可以将步骤分解为两步:

  1. 找到最后一个复写的元素;

    找到最后一个复写的元素这里采用的是一个双指针的方式;

    即使用双指针将复写的步骤模拟进行一遍,cur指针用来遍历,dest指针用来记录(模拟)复写;

    由于需要找到的是完成复写时的位置,所以定义dest时定义为-1,而cur用于遍历,所以在0位置处;

    可以分为四步:

    • 判断cur所指向的数据是否为0,若是为0则复写2次(dest+=2);
    • 若是为非0则复写1次(dest++);
    • 判断dest是否完成所有复写;
    • 继续遍历cur指针;
    /*
    *找到最后一个复写的位置
    */
    	int len = arr.size();
    	int cur = 0;
    	int dest = -1;
    	for(;dest<len-1;++cur){//由于是需要找到位置的下标,所以需要控制至size()-1的位置即下标的位置;
    		if(arr[cur]==0){
    			dest+=2;
    		}
    		else{
    			++dest;
    		}
    		if(dest>=len-1) break;
    	}
    
  2. 根据规则由后向前进行复写;

     for(;cur>-1||dest>-1;--cur,--dest){
                arr[dest] = arr[cur];
                if(arr[cur] == 0){
                    arr[--dest] = 0;
                }
            }
    

但是很不巧的是若是以这样的方式提交代码仍然会报错;

报错的原因一样是因为越界;

假设有这样一组数据:[ 8 , 4 , 5 , 0 , 0 , 0 , 0 , 7 ];

在为该数据找最后一个复写的数据时将会出现这样的问题:

在该测试用例中,由于多次进行了2次复写0的操作从而导致了最终dest指针越界;

所以在这里应该进行边界控制;

在该题中越界的程度只能是1(只存在步长为1或者步长为2的操作);

if(dest == len){
            arr[len-1] = 0;
            --cur,dest-=2;//由于dest已经进行越界,所以要多跳一步;
        }

代码

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        /*
        *找到最后一个复写的位置
        */
        int len = arr.size();
        int cur = 0;
        int dest = -1;
        for(;dest<len-1;++cur){
            if(arr[cur]==0){
                dest+=2;
            }
            else{
                ++dest;
            }
            if(dest>=len-1) break;
        }
        cout<<cur<<" "<<dest<<" "<<len<<endl;//检查最后复写位置是否正确

        //---------------------

        if(dest == len){//因为移动只有1步与2步,在进行移动的过程中只会出现越界1位的情况;
        //这里越界的情况大部分属于最后一位是0从而导致的越界1位的现象,那对于最后一个边界处理的情况只要在len-1的位置即最后一个位置复写一次0即可;
        //复写一次0后代表最后一个0已经被复写完毕,所以要跳过该0;
            arr[len-1] = 0;
            --cur,dest-=2;//由于dest已经进行越界,所以要多跳一步;
        }

        //---------------------
        for(;cur>-1||dest>-1;--cur,--dest){
            arr[dest] = arr[cur];
            if(arr[cur] == 0){
                arr[--dest] = 0;
            }
        }
    }
};



快乐数

题目解析

【题目链接】

题目的要求是判断一个数是否为快乐数;

同时给出了关于快乐数的规则:

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

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


算法解析

在这道题目中可以用以下两种方式进行解决;

  • unordered_map 哈希暴力法;

    该方法即使用unordered_map 容器用于判断是否有数据重复;

    若是重复则返回并判断重复的值是否为1;

  • 快慢双指针法;

    该方法则使用到了一个思路,即快慢指针;

    以题目中给出的两个例子进行分析;

    1. 19

      12 + 92 = 82; 82 + 22 = 68;

      62 + 82 = 100; 12 + 02 = 1;

      12 + 02 = 1; …

      用图表示即为

    1. 2

      22 = 4; 42 = 16;

      12 + 62 = 37; …

      12 + 42 + 52 = 42; 42 + 22 = 20;

      22 + 02 = 4;

      用图表示即为

      从两图可知这里将会形成一个类似于带环链表的结构;

      两处都必定会在同一处进行循环;

      而这题与判断链表是否带环唯一的不同之处就是该题只需要判断相遇或者循环的位置是否为1即可;

    拓展

    在这一题目的描述之中,有个奇妙的点,为什么在这个规则之下必定会出现循环且不会有第三种结果(即无限不循环);

    鸽巢原理(抽屉原理)

    假设鸽巢的数量为n而鸽子的数量为n+1,则必定会有格子数量大于1的鸽巢;

    同时假设n为9999999999 , 以快乐数的规则而言,该数为 92 * 10 为810;

    换言之它的区间即为[ 1 , 810 ];

    当经过810次的上述操作后,再第811次时的操作必定会重复;

    所以不会存在第三种结果;


代码

unordered_map暴力法

class Solution {
public:
    bool isHappy(int n) {
        if(n == 1){//特殊例子所作处理 " 1 "
            return true;
        }
        
        unordered_map<int,int> _ret;
        int ret = 0;
        while(n!=1){
            int sum = 0;
            int tmp = n;
            while(n){
                sum+=(n%10)*(n%10);
                n/=10;
            }
            n = sum;
            ++_ret[n];
            if(n == 1) return true;//如果n为1提前结束
            for(auto [num,count]:_ret){
                if(count>=2){
                    return false;//用于判断是否无限循环
                }
            }
        }
        return false;//防止报错所作处理
    }
};

快慢双指针法

class Solution {
public:

    int RetSum(int n){//将操作单独封装为一个子函数
        int sum = 0;
        while(n){
            sum += (n%10) * (n%10);
            n/=10;
        }
        return sum;
    }

    bool isHappy(int n) {
        size_t slow = n;
        size_t fast = n;
        do{
            fast = RetSum(RetSum(fast));
            slow = RetSum(slow);
        }
        while(slow!=fast);
        if(slow == 1){
            return true;
        }
        return false;
    }
};



盛最多水的容器

题目解析

【题目链接】

该题目中要求计算盛水最多的容器;

即要求计算x轴与y轴乘积最大的一次;

以给出的实例1

[ 1 , 8 , 6 , 2 , 5 , 4 , 8 , 3 , 7 ]

在该示例中的答案为y轴为8与7时;

x轴长度为8-1 = 7时的乘积为49;


算法解析

在该题目中有两种解法

  • 暴力枚举法

    对于暴力枚举来说即定义两个指针,利用嵌套for循环枚举出每一个可能性并找到Max最大值;

    时间复杂度为O(N2);

    该时间复杂度在该题中将会超出时间限制(2w数量的测试用例);

  • 双指针遍历

    该方法则考虑到一个规律;

    以该段数组中的小区间[ 8 , 6 , 2 , 5 , 4 ]为例;

    在这个区间内的盛水量为 4*(4-0) = 16;

    假设以右边高度 4 进行枚举(与 6 , 2 , 5 )则会出现两种状况(由于始终是向内进行枚举所以宽度w不断在缩小):

    1. 当碰到比4要小的高度时高度h减小,容积v = h*w也在减小;
    2. 当碰到比4要大的高度时,高度不变,但由于w在缩小所以容积v一样会缩小;

    通过上面两种结果可以直接判断,当高度不同时优先舍弃高度较低的高度枚举;


代码

  • 暴力枚举法

    class Solution {
    public:
        int maxArea(vector<int>& height) {
            int maxR = 0;//记录当前所记录的最大容量
            for(int i = 0;i<height.size();++i){
                for(int j = i;j<height.size();++j){
                    int min = height[j]<height[i]?height[j]:height[i];
                    int tmp = (j-i)*min;
                    if(tmp>maxR) maxR = tmp;
                }
            }
            return maxR;
    
        }
    };
    
  • 双指针遍历法

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



有效的三角形个数

题目解析

【题目链接】

该题题目为给定一个数组,计算出数组中能组成三角形的三元组合;

三角形的判定方式:三角形的任意两条边都大于第三便;


算法原理

该题共有三种方法分别为:

  • 暴力法
  • 二分法
  • 双指针法

在该处主要讲解暴力枚举与双指针法;

  • 暴力枚举

    暴力枚举法即定义三个指针分别指向三条边;

    将所有的可能都进行枚举,并将可以组成三角形的三元组和以计数器的方式记录下来;

    //伪代码
    check(i,j,k){
        i+j>k&&
            i+k>j&&
            	j+k>i;
    }
    
    main(){
    	for(i;i<size;++i){
        	for(j;j<size;++j){
            	for(k;k<size;++k){
                	check()
            	}
        	}
    	}
    }
    

    由于每次的check中都需要进行三次判断,所以对于该方式而言时间复杂度为 O(3 * N3);

    当然可以在该方法为前提中进行优化;

    1. 优化

      从三角形的规则可以推断出,若是两条较短的边大于第三条边的话那么这个三元组和必定能组成一个三角形;

      而当我们将数组进行排序过后即可以只进行一次判断

      check(){
          i+j>k?
      }
      
      main(){
          sort(begin(),end());
          for(){
              for(){
                  for(){
                      check();
                  }
              }
          }
      }
      

    在优化过后,暴力解法的时间复杂度将会由O(3 * N3)变为O(N*logN + N3);

  • 双指针解法

    双指针解法主要还是对数组的排序,由于数组被排序后数据具有了单调性;

    所以可以以该单调性为基础做出各种优化;

    假设有数据为[ 2 , 11 , 9 , 5 , 3 , 2 , 4 ];

    在进行排序之后为[ 2 , 2 , 3 , 4 , 5 , 9 , 11 ];

    假设有指针cur指向最右值(最大)作为第三条边;

    并设指针left = 0,right = cur-1;

    此时将左右当作两条边与cur第三条边作比较,将会有两种可能:

    1. left + right > cur 三元组成立
    2. left + right <= cur 不成立

    以该数据为例,左右相加并不能满足条件时,++left;

    当left指向下标2的位置,也就是3时,左右相加大于cur( 3 + 9 = 12 > 11 );

    此时则表示满足了最低条件,而右侧属于由于有序(大于left所指向的数据),则可以表示[ left , right )区间内的所有数据都满足条件;

    当该处处理结束时则表示right为8的匹配条件已经判断结束,right向前走1位进行下一步的判断;

    当走到该处时left指针与right指针的和并不能组成三角形;

    所以表示left所指向的数据与当前right所在的数据并不能组成一个合规的三元组和,所以left++(由于具有单调性,表示该left所指向位置的数据与右侧的任意数据相加都不能组成一个合规的三元组和);

    当该段区间全部检查完毕之后,cur朝前走一步判断下一个区间;

    简单来说可以分为两个步骤:

    • 固定最大的数
    • 在最大的数的左区间内使用双指针,快速统计出符合要求的三元组和;

    该解法的时间复杂度为O(N*logN + N2);


代码

暴力解法不予代码;

双指针法:

class Solution {
public:
    
    int triangleNumber(vector<int>& nums) {
    
        sort(nums.begin(),nums.end());//排序
        int count = 0;
        int cur = nums.size()-1;//固定最初最大值位置

        while(cur>1){
        int left = 0;//每次的left与right都要进行新的初始化
        int right = cur-1;
        cout<<right<<endl;
            while(left<right){
                if(nums[left]+nums[right]>nums[cur]){
                    count+=right-left;
                    right--;
                }
                else{
                    ++left;
                }
            }
        --cur;
        }
        return count;
    }
};



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

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

相关文章

想要精通算法和SQL的成长之路 - 最小高度树

想要精通算法和SQL的成长之路 - 最小高度树 前言一. 最小高度树1.1 邻接表的构建1.2 入度为1的先入队1.3 BFS遍历 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 最小高度树 原题链接 从题目的含义中我们可以发现&#xff1a; 题目的树是一颗多叉树。叶子节点的度为1&a…

你的支付环境是否安全?

1、平台支付逻辑全流程分析分析 2、平台支付漏洞如何利用&#xff1f;买东西还送钱&#xff1f; 3、BURP抓包分析修改支付金额&#xff0c;伪造交易状态&#xff1f; 4、修改购物车参数实现底价购买商品 5、SRC、CTF、HW项目月入10W副业之路 6、如何构建最适合自己的网安学习路…

【项目经理】目标管理工具

目标管理工具 1. WBS 任务分解法&#x1f44a;原则方法标准 2. 6W2H法WhatwhyWhowhen⏲️WhereWhichHowHow much 3. SWOT分析法strengths-优势Weaknesses-劣势Opportunities-机会Threats-威胁 4. 二八原则法巴列特定律准则例子 5. SMART原则SpecificMeasurableAttainableReleva…

处于十字路口的CIO:继续进化还是走进死胡同

2023年初Forrester研究给出的一个坏消息表明&#xff0c;有很多CIO尚未准备好满足这些新的需求。大多数CIO&#xff08;58%&#xff09;仍处于Forrester所说的传统IT领导模式&#xff1b;有37%的CIO被认为是“现代的”&#xff0c;但只有6%的CIO是“适合未来的”&#xff0c;具…

YOLOv8优化:独家创新(SC_C_Detect)检测头结构创新,实现涨点 | 检测头新颖创新系列

💡💡💡本文独家改进:独家创新(SC_C_Detect)检测头结构创新,适合科研创新度十足,强烈推荐 SC_C_Detect | 亲测在多个数据集能够实现大幅涨点 💡💡💡Yolov8魔术师,独家首发创新(原创),适用于Yolov5、Yolov7、Yolov8等各个Yolo系列,专栏文章提供每一步步…

面试了上百位性能测试后,我发现了一个令人不安的事实

在企业中负责技术招聘的同学&#xff0c;肯定都有一个苦恼&#xff0c;那就是招一个合适的测试太难了&#xff01;若要问起招哪种类型的测试最难时&#xff0c;相信很多人都会说出“性能测试”这个答案。 每当发布一个性能测试岗位&#xff0c;不一会就能收到上百份简历&#x…

开发者版 ONLYOFFICE 文档 7.5:API 和文档生成器更新

随着版本 7.5 中新功能的发布&#xff0c;我们更新了编辑器、文档生成器、插件和桌面应用程序的 API。阅读本文查看所有详细信息。 用于处理表单的 API 隐藏/显示提交表单按钮&#xff1a;使用 editorConfig.customization.submitForm 参数&#xff0c;可以定义 OFORM 文件的顶…

【CV】图像分割详解!

图像分割是计算机视觉研究中的一个经典难题&#xff0c;已经成为图像理解领域关注的一个热点&#xff0c;图像分割是图像分析的第一步&#xff0c;是计算机视觉的基础&#xff0c;是图像理解的重要组成部分&#xff0c;同时也是图像处理中最困难的问题之一。所谓图像分割是指根…

【量化交易笔记】12.海龟交易策略

引言 海龟交易法则是一种著名的趋势跟踪交易策略&#xff0c;适用于中长线投资者。 海龟交易策略&#xff08;Turtle Trading&#xff09;起源于美国&#xff0c;由著名的交易员理查德丹尼斯&#xff08;Richard Dennis&#xff09;创立。这种交易策略属于趋势跟踪策略&#…

Quirks(怪癖)模式是什么?它和 Standards(标准)模式有什么区别?

目录 前言: 用法: 代码: Quirks模式示例: Standards模式示例: 理解: Quirks模式&#xff1a; Standards模式&#xff1a; 高质量讨论: 前言: "Quirks模式"和"Standards模式"是与HTML文档渲染模式相关的两种模式。它们影响着浏览器如何解释和渲染HT…

华夏版-超功能记事本 Ⅲ 8.8易语言源码

华夏版-超功能记事本 Ⅲ 8.8易语言源码 下载地址&#xff1a;https://user.qzone.qq.com/512526231

VisualStudio[WPF/.NET]基于CommunityToolkit.Mvvm架构开发

一、创建 "WPF应用程序" 新项目 项目模板选择如下&#xff1a; 暂时随机填一个目标框架&#xff0c;待会改&#xff1a; 二、修改“目标框架” 双击“解决方案资源管理器”中<项目>CU-APP, 打开<项目工程文件>CU-APP.csproj, 修改目标框架TargetFramew…

windows开机自启动和忘记密码-备忘

windows开机自启动和忘记密码-备忘 文章目录 windows开机自启动和忘记密码-备忘1.自启动网址定时任务方式 2.忘记windows用户密码 1.自启动 网址 参考博文&#xff1a;https://blog.csdn.net/wwzmvp/article/details/113656544&#xff0c;感谢博主。 定时任务方式 如图&#…

uniapp如何跳转系统授权管理页?

如何跳转系统授权管理页&#xff1f; 跳转APP应用授权设置页面 文章目录 如何跳转系统授权管理页&#xff1f;效果图打开系统App的权限设置界面 效果图 例&#xff1a;Android 打开系统App的权限设置界面 App端&#xff1a;打开系统App的权限设置界面微信小程序&#xff1a;打开…

20231024后端研发面经整理

1.如何在单链表O(1)删除节点&#xff1f; 狸猫换太子 2.redis中的key如何找到对应的内存位置&#xff1f; 哈希碰撞的话用链表存 3.线性探测哈希法的插入&#xff0c;查找和删除 插入&#xff1a;一个个挨着后面找&#xff0c;知道有空位 查找&#xff1a;一个个挨着后面找…

express session

了解 Session 认证的局限性 Session 认证机制需要配合 cookie 才能实现。由于 Cookie 默认不支持跨域访问&#xff0c;所以&#xff0c;当涉及到前端跨域请求后端接口的时候&#xff0c;需要做很多额外的配置&#xff0c;才能实现跨域 Session 认证。 注意&#xff1a; 当前端…

Unity实现方圆多少米范围随机生成怪物

using System.Collections; using System.Collections.Generic; using UnityEngine;public class CreatMonster : MonoBehaviour {// S这个脚本间隔一点时间生成怪物/*1.程序逻辑* 1. 设计一个计时器* 2.间隔一段时间3s执行一下 * */float SaveTime 0f;public GameObject …

【C++笔记】C++继承

【C笔记】C继承 一、继承的概念二、继承的语法和权限三、父类和子类成员之间的关系3.1、子类赋值给父类(切片)3.2、同名成员 四、子类中的默认成员函数4.1、构造函数4.2、拷贝构造4.3、析构函数 五、C继承大坑之“菱形继承”5.1、什么是“菱形继承”5.2、解决方法 一、继承的概…

为什么自学或是培训完软件测试后,找不到工作?原因可能是这几种

最近我的一个表弟想学习软件测试&#xff0c;但他一直在犹豫是报班还是自学&#xff0c;甚至担心学完后市场饱和了&#xff0c;学完找不到工作。那么借用这次机会&#xff0c;跟大家进行分析一下软件测试行业找不到工作的几个原因&#xff0c;希望能够帮助到大家&#xff0c;少…

Python(一)关键字、内置函数

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&am…