【基础算法总结】双指针

2024-09-07 04:52
文章标签 算法 基础 指针 总结

本文主要是介绍【基础算法总结】双指针,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 一,双指针算法介绍
  • 二,算法原理和代码实现
    • 283.移动零
    • 1089.复写零
    • 202.快乐数
    • 11.盛最多水的容器
    • 611.有效三角形的个数
    • LRC179.和为s的两个数
    • 15.三数之和
    • 18.四数之和
  • 三,算法总结

一,双指针算法介绍

双指针算法是基础算法之一,一般用于涉及数组分块/数组划分这类问题 。这里的"指针"是利用数组下标或是一个数来充当的

在遍历过程中,两个指针的位置:
cur:从左往右扫描数组,遍历数组
dest:指向已处理的区间内,非0元素的最后一个位置。如下图
在这里插入图片描述
所以两个指针把数组分成了三个区间
在这里插入图片描述

二,算法原理和代码实现

283.移动零

在这里插入图片描述

在这里插入图片描述

算法原理:

我们也是定义两个变量 cur 和 dest,根据上面介绍的两个指针的位置初始化 cur = 0, dest = -1

在 cur 从前往后遍历的过程中,无非两种情况
1. 遇到0元素:cur++
2. 遇到非0元素:先swap(dest+1, cur), 再cur++, dest++

代码实现:

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

1089.复写零

在这里插入图片描述
在这里插入图片描述

算法原理:

这道题看起来简单,但是有很多坑,很多细节。我们使用双指针先在草稿纸上模拟,不难发现从前往后复写是不行的,会覆盖后面的数据。但是要如何从后往前复写呢,起始位置怎么确定

所以解决这个题有两个步骤:

1. 先找到最后一个复写的数
这一步骤也要用双指针算法:
在这里插入图片描述
当走完这个双指针,此时 cur 指向的数就是最后一个要复写的数,dest 指向的位置就是开始复写的第一个位置
2. 再从后往前进行复写操作
在 cur 从后往前遍历的过程中,无非两种情况:
(1) 遇到0元素:dest向前复写两个0,cur–,dest -= 2
(2) 遇到非0元素:先arr[dest] = arr[cur], 再cur++, dest++

细节/技巧问题:

(1) 在第一步的第三小步中一定要先判断 dest 是否已经结束
(2) 还要处理一种特殊情况:[1,0,2,3,0,4]。根据第一步的双指针,此时dest会越界,就要做特殊处理当 dest == n 时,直接把 arr[n-1] = 0, cur–, dest -= 2

代码实现:

class Solution
{
public:void duplicateZeros(vector<int>& arr){// 找到最后一个要复写的位置int cur = 0, dest = -1, n = arr.size();while (cur < n){if (arr[cur]) dest++;else dest += 2;if (dest >= n - 1) break;cur++; // 注意每次++之前都要先判断dest是否越界}// 处理特殊情况if (dest == n) arr[n - 1] = 0, cur--, dest -= 2;// 从后往前开始复写操作while (cur >= 0){if (arr[cur]) arr[dest--] = arr[cur--];else{arr[dest] = 0, arr[dest - 1] = 0;dest -= 2;cur--;}}}
};

下面是一开始我写的错误代码,以示警戒

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

202.快乐数

在这里插入图片描述

在这里插入图片描述

算法原理:

首先来理解题目
在这里插入图片描述
所以这道题可以抽象成另类的 “链表是否带环问题”只不过这道题是一定带环的,只要根据环内的数进行判断即可

使用经典的快慢双指针算法
(1) 定义快慢指针
(2) 慢指针每次向后走一步,快指针向后走两步
(3) 判断相遇时候的值即可

拓展:为什么这个题一定成环?

使用鸽巢原理证明
在这里插入图片描述

细节/技巧问题:

(1) 这题的双指针不再是数组下标,而是一个数
(2) 在进入第一次循环时,先让 fast 指向第二个数,不然进不了循环

代码实现:

class Solution 
{
public:bool isHappy(int n) {int slow = n, fast = mypow(n);while(slow != fast){slow = mypow(slow);fast = mypow(mypow(fast));}if(slow == 1)return true;elsereturn false;}int mypow(int n){int sum = 0;while(n){int a = n % 10;n /= 10;sum += a*a;}return sum;}
};

11.盛最多水的容器

在这里插入图片描述
在这里插入图片描述

算法原理:

解法1:暴力枚举,O(N*N)。这应该是大家心中第一个闪过的想法,就是使用两层for循环计算出全部体积,再求出最大值。但是这个解法超时。

解法2:利用单调性,使用双指针,O(N)
首先观察一个规律:
我们固定一个数,再向外枚举,会出现下面的情况,结果都是缩小,就可以把这个数直接抹去,避免无用枚举
在这里插入图片描述
所以可以把这个规律推广到全部数据:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/a1dee81bedce4fab936c08ca8ba7f279.png
定义两个指针指向开头和结尾,此时可以计算出一个体积,根据上面的规律把较小的那个高度直接舍去,指针向里缩,继续计算体积…,直到两个指针相遇,统计出最大体积

细节/技巧问题:

体积= 高度 * 宽度。根据木桶原理,高度是小的那个,而宽度是两个下标相减

代码实现:

class Solution 
{
public:int maxArea(vector<int>& height) {int cur1 = 0, cur2 = height.size() -1;int maxV = 0;while(cur1 <= cur2){int H = min(height[cur1], height[cur2]);maxV = max(maxV, (cur2 - cur1) * H);// 利用单调性if(height[cur1] < height[cur2]) cur1++;else cur2--;}return maxV;}
};

611.有效三角形的个数

在这里插入图片描述
在这里插入图片描述

算法原理:

首先补充一个数学知识,只要一次判断,得出是否能构成三角形

若a <= b <= c 且 a +b > c,则可以构成三角形
所以可以得出一个优化:先把数组排序,O(N*logN)

数组排完序后

解法1:暴力枚举,O(N*logN + N^3),三层for循环,绝对超时。

解法2:利用单调性,使用双指针算法,O(N*logN + N^2)
(1) 先用c固定最大的元素,定义left 和right分别指向第一个元素,c的下一个元素。
(2) 若nums[left] + nums[right] > nums[c],由于left和 right之间的数都比 nums[left] 大,与 nums[right] 相加后一定大于c,构成三角形了,就不要一个个枚举了,直接right- -
(3) 此时三角形个数 = right - left
(4) 若nums[left] + nums[right] <= nums[c],由于left和 right之间的数都比 nums[right] 小,与 nums[left] 相加后一定小于c,也不要一个个枚举了,直接left++
(5) 以上是走完一趟的个数,再c–,固定下一个数
在这里插入图片描述

代码实现:

class Solution 
{
public:int triangleNumber(vector<int>& nums) {// 排序sort(nums.begin(), nums.end());int n = nums.size();int ret = 0; // 记录个数for(int c = n-1; c >= 2; c--){int left = 0, right = c - 1;while(left < right){if(nums[left] + nums[right] > nums[c]){ret += right - left;right--;} else left++;}}return ret;}
};

LRC179.和为s的两个数

在这里插入图片描述
在这里插入图片描述

算法原理:

解法1:暴力枚举,两层for循环,O(N^2),绝对超时。没有好好利用单调递增这个特性!!

解法2:和上一题的解法2类似,也是利用单调性和双指针算法,O(N)
在这里插入图片描述

细节/技巧问题:

找到数对后就直接 break 结束循环

代码实现:

class Solution 
{
public:vector<int> twoSum(vector<int>& price, int target) {vector<int> ret;int left = 0, right = price.size() - 1;while(left < right){int sum = price[left] + price[right];if(sum > target) right--;else if(sum < target) left++;else {ret.push_back(price[left]); ret.push_back(price[right]);break;}}return ret;}
};

15.三数之和

在这里插入图片描述
在这里插入图片描述

算法原理:

解法1:排序+暴力枚举+利用set去重,O(N^3).

解法2:排序+双指针,O(N^2).
这道题其实是上一题的进阶版,首先排序,再固定一个数 a,在该数后面的区间内使用双指针算法快速的找到两个数的和是 -a
在这里插入图片描述

难点/细节/技巧:

这道题的算法不难想,难的是保证不重不漏

(1) 去重操作有两个方面:一是找到一种结果后,left 和 right 指针要跳过重复元素,二是当使用完一次双指针后,i 也需要跳过重复元素,此时要注意越界
(2) 还有一个小优化就是当固定的那个数是正数时,后面再也找不到两数和为负数了,直接结束

代码实现:

class Solution 
{
public:vector<vector<int>> threeSum(vector<int>& nums){// 排序sort(nums.begin(), nums.end());vector<vector<int>> vv;int n = nums.size();for(int i = 0; i <= n-3; i++) // 固定数 a{if(nums[i] > 0) break; // 小优化int left = i +1, right = n-1, a = nums[i];// 双指针while(left < right){int sum = nums[left] + nums[right];if(sum > -a) right--;else if(sum < -a) left++;else {vv.push_back({nums[i], nums[left], nums[right]});// 去重,注意越界while(left < right && nums[left] == nums[left+1]) left++;while(left < right && nums[right] == nums[right-1]) right--;left++;right--;}}while(i < n-1 && nums[i] == nums[i+1]) i++;}return vv;}
};

18.四数之和

在这里插入图片描述
在这里插入图片描述

算法原理:

本道题又是上一题的进阶版

在这里插入图片描述

难点/细节/技巧:

这道题会出现数据溢出的风险。所以当计算两个固定是数之和时,类型定义为 long long
其余细节参考上一题

代码实现:

class Solution 
{
public:vector<vector<int>> fourSum(vector<int>& nums, int target) {// 排序sort(nums.begin(), nums.end());int n = nums.size();vector<vector<int>> vv;for(int i = 0; i < n; i++) // 固定数a{// 三数和for(int j = i+1; j < n; j++)// 固定数b{// 双指针算法int left = j+1, right = n-1;long long t = (long long)target - (nums[i] + nums[j]);while(left < right){long long sum = nums[left] + nums[right];if(sum > t) right--;else if(sum < t) left++;else{vv.push_back({nums[i], nums[j], nums[left], nums[right]});// 去重while(left < right && nums[left] == nums[left+1]) left++;while(left < right && nums[right] == nums[right-1]) right--;left++;right--;}}while(j < n-1 && nums[j] == nums[j+1]) j++;}while(i < n-1 && nums[i] == nums[i+1]) i++;}return vv;}
};

三,算法总结

双指针算法是一种基础,但是十分经典的算法。通过上面的若干道题可知,"双指针"使用起来是十分灵活的,有时代指数组下标,有时也可以代指一个数使用双指针算法的关键之一就是要控制好边界,稍不留神就会出现数组越界的问题并且在使用这个算法时强烈建议各位一定要多画图,光靠想象容易出错并且会忽略很多细节问题

这篇关于【基础算法总结】双指针的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1144111

相关文章

Python中logging模块用法示例总结

《Python中logging模块用法示例总结》在Python中logging模块是一个强大的日志记录工具,它允许用户将程序运行期间产生的日志信息输出到控制台或者写入到文件中,:本文主要介绍Pyt... 目录前言一. 基本使用1. 五种日志等级2.  设置报告等级3. 自定义格式4. C语言风格的格式化方法

Spring 依赖注入与循环依赖总结

《Spring依赖注入与循环依赖总结》这篇文章给大家介绍Spring依赖注入与循环依赖总结篇,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. Spring 三级缓存解决循环依赖1. 创建UserService原始对象2. 将原始对象包装成工

从基础到高级详解Python数值格式化输出的完全指南

《从基础到高级详解Python数值格式化输出的完全指南》在数据分析、金融计算和科学报告领域,数值格式化是提升可读性和专业性的关键技术,本文将深入解析Python中数值格式化输出的相关方法,感兴趣的小伙... 目录引言:数值格式化的核心价值一、基础格式化方法1.1 三种核心格式化方式对比1.2 基础格式化示例

redis-sentinel基础概念及部署流程

《redis-sentinel基础概念及部署流程》RedisSentinel是Redis的高可用解决方案,通过监控主从节点、自动故障转移、通知机制及配置提供,实现集群故障恢复与服务持续可用,核心组件包... 目录一. 引言二. 核心功能三. 核心组件四. 故障转移流程五. 服务部署六. sentinel部署

从基础到进阶详解Python条件判断的实用指南

《从基础到进阶详解Python条件判断的实用指南》本文将通过15个实战案例,带你大家掌握条件判断的核心技巧,并从基础语法到高级应用一网打尽,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录​引言:条件判断为何如此重要一、基础语法:三行代码构建决策系统二、多条件分支:elif的魔法三、

Python WebSockets 库从基础到实战使用举例

《PythonWebSockets库从基础到实战使用举例》WebSocket是一种全双工、持久化的网络通信协议,适用于需要低延迟的应用,如实时聊天、股票行情推送、在线协作、多人游戏等,本文给大家介... 目录1. 引言2. 为什么使用 WebSocket?3. 安装 WebSockets 库4. 使用 We

MySQL中查询和展示LONGBLOB类型数据的技巧总结

《MySQL中查询和展示LONGBLOB类型数据的技巧总结》在MySQL中LONGBLOB是一种二进制大对象(BLOB)数据类型,用于存储大量的二进制数据,:本文主要介绍MySQL中查询和展示LO... 目录前言1. 查询 LONGBLOB 数据的大小2. 查询并展示 LONGBLOB 数据2.1 转换为十

从基础到高阶详解Python多态实战应用指南

《从基础到高阶详解Python多态实战应用指南》这篇文章主要从基础到高阶为大家详细介绍Python中多态的相关应用与技巧,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、多态的本质:python的“鸭子类型”哲学二、多态的三大实战场景场景1:数据处理管道——统一处理不同数据格式

MySQL数据类型与表操作全指南( 从基础到高级实践)

《MySQL数据类型与表操作全指南(从基础到高级实践)》本文详解MySQL数据类型分类(数值、日期/时间、字符串)及表操作(创建、修改、维护),涵盖优化技巧如数据类型选择、备份、分区,强调规范设计与... 目录mysql数据类型详解数值类型日期时间类型字符串类型表操作全解析创建表修改表结构添加列修改列删除列

Python 函数详解:从基础语法到高级使用技巧

《Python函数详解:从基础语法到高级使用技巧》本文基于实例代码,全面讲解Python函数的定义、参数传递、变量作用域及类型标注等知识点,帮助初学者快速掌握函数的使用技巧,感兴趣的朋友跟随小编一起... 目录一、函数的基本概念与作用二、函数的定义与调用1. 无参函数2. 带参函数3. 带返回值的函数4.