双指针算法详解:从原理到实战(含LeetCode经典例题)

双指针算法详解:从原理到实战(含LeetCode经典例题)

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

在这里插入图片描述

🌈say-fall:个人主页🚀专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》💪格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言:

基于数据结构的扎实基础,算法思想能够有效提升问题解决的效率。为此,我们开设一个专门的算法专栏,用来探讨各类算法题目的解决方案。

在算法学习的道路上,双指针是一种简洁、高效且应用广泛的解题思想,它并非局限于某一种特定的数据结构,而是一种通过设置两个“标记点”(指针),协同遍历、筛选或修改数据,从而简化问题复杂度的核心思路。无论是数组、链表的遍历处理,还是数值组合、环形问题的求解,双指针都能发挥其独特优势——相较于暴力枚举的多层循环,它往往能将时间复杂度从O(n²)优化至O(n),大幅提升代码运行效率,同时让解题逻辑更清晰、代码更简洁易懂。
本文将系统拆解双指针算法的核心逻辑,按“快慢双指针”“首尾双指针”“快慢双指针解决环形问题”三大类别,结合LeetCode经典例题(如移动零、盛水最多的容器、三数之和、环形链表等),从算法思想、解题步骤、代码实现三个维度,手把手带你掌握双指针的用法。每道例题均搭配详细解析和优化思路,兼顾基础入门与进阶提升,帮助你真正理解双指针的核心逻辑。


文章目录


正文:

零、什么是双指针

需要说明的是,双指针并不局限于字面意义上的两个指针,而是通过标记两个目标点来简化问题的解决思路。接下来,我们通过具体实例深入解析这一算法。

一、快慢双指针

0. 算法思想:

前后快慢指针以慢指针为基准划分界限:慢指针左侧(包括其指向的元素)均小于等于基准值,右侧均大于基准值;或者是慢指针左或者右为同一元素。

1. 双指针快排思想

在这里插入图片描述


快速排序最初由霍尔(Hoare)提出,其算法逻辑相对复杂。为便于理解,后来发展出了前后指针法实现快排。以下是详细说明:

注:本文提到的"指针"并非真正意义上的指针,而是数组的索引下标关键概念说明:
key [基准值]
prev [慢指针]
cur [快指针]
  1. 首先选择最左侧元素作为基准值 key,初始化 prev 指针与 key 同位置,cur 指针指向下一个位置
  2. cur 指向的元素小于 key 时,移动 prev 指针并与 cur 进行元素交换(可以避免原地互换)
  3. cur 指针持续移动,遇到比基准值小的元素时暂停,此时移动 prev 指针并进行交换(非原地交换会将较小元素移至 prev 位置)
  4. 重复上述过程直到 cur 指针越界
  5. 最后交换 keyprev 指向的元素,确保基准值左侧元素均小于它,右侧元素均大于它
//双指针法快排intptrSort(int* a,int left,int right){int keyi = left;int prev = left, cur = left +1;while(cur <= right){if(a[cur]< a[keyi]){ prev++;if(a[cur]!= a[prev]){Swap(&a[cur],&a[prev]);}} cur++;}Swap(&a[keyi],&a[prev]);return prev;}

至此完成对基准值的排序,之后只需递归地对左右两个子区间执行相同操作(一般使用递归),即可完成整个数组的排序。

2. 双指针移动零

题目:leetcode:移动零

在这里插入图片描述

移动零问题可以类比为以零为基准的单次快速排序:

  1. 初始化两个指针:cur指向数组首元素,prev初始化为-1
  2. 遍历过程中:
    • 当cur遇到非零元素时,prev右移一位并与cur交换元素
    • 否则cur继续右移
  3. 重复上述过程直至cur到达数组末尾
classSolution{public:voidmoveZeroes(vector<int>& nums){for(int cur =0,dest =-1;cur<nums.size();cur++)if(nums[cur])swap(nums[++dest],nums[cur]);}};

3. 复写零

在这里插入图片描述


处理复写零问题的关键在于确定结束位置。以下是优化后的双指针解决方案:

  1. 初始化指针:将cur和dest都指向数组首元素,然后cur开始移动
  2. 遍历规则:
    • 当cur指向0时:dest移动两步
    • 当cur指向非零元素时:dest移动一步
  3. 终止条件:当dest到达数组末尾时停止
  4. 边界处理:
    • 若cur指向最后一个元素为0时,dest可能越界
    • 此时需要将dest回退两位,并将末尾元素置零,同时cur回退一位
  5. 逆向复写:由于已精确计算复写元素数量,无需担心原地复写导致的覆盖问题
classSolution{public:voidduplicateZeros(vector<int>& arr){//双指针找最后一个复写的数int cur =0,dest =-1,n = arr.size();while(1){if(arr[cur]==0) dest++; dest++;if(dest >= n-1)break;//找到退出 cur++;}//处理特殊情况if(dest == arr.size()){ cur--; dest -=2; arr[arr.size()-1]=0;}//从后向前复写while(cur >=0){if(arr[cur]==0) arr[dest--]= arr[cur]; arr[dest--]= arr[cur]; cur--;}}};

三、首尾双指针

0. 算法思想:

首尾分别设定指针,可以交换元素解决移动零类似问题;也可以利用单调性解决其他问题

1. 双指针移动零

题目:leetcode:移动零

在这里插入图片描述


如果不需要保持元素的先后顺序的话,也可以使用首尾双指针法

  1. 设置左右指针,leftright
  2. left 找0,right 找非零元素,交换
  3. 直到左右指针相遇
classSolution{public:voidmoveZeroes(vector<int>& nums){int left =0,right = nums.size()-1;while(left < right){if(nums[left]) left++;if(nums[right]==0) right--;if(left < right && nums[left]==0&& nums[right]!=0)swap(nums[left],nums[right]);}}};
在这里插入图片描述

2. 盛水最多的容器

  • 解法一:
    本题可以使用暴力解法来计算,即两层循环计算出 “体积” , 取其最大值。
  • 解法二:
    根据体积的计算公式 v = w * h :

题目:leetcode:盛水最多的容器

在这里插入图片描述
  1. 先排序,得到一个有顺序的数组
  2. 首尾设定指针,向内移动,宽度w必定减小,高度由于取最小值,会降低或者不变
  3. 则说明:计算出体积v以后,将二者高度较小的一方排除,向内移动,即可减少遍历次数
classSolution{public:intgetv(vector<int>& height,int left,int right){int width = right - left;int high = height[right];//假设法if(height[left]< height[right]){ high = height[left];}return width * high;}intmaxArea(vector<int>& height){int left =0;int right = height.size()-1;int v =0;while(left < right){int vv =getv(height, left, right);if(height[left]< height[right]) left++;else right--;if(vv > v) v = vv;}return v;}};

3. 两数之和

  • 解法一:
    双层循环,暴力枚举出所有可能值,找到两数之和为目标的两个数。
  • 解法二:

题目:两数之和

在这里插入图片描述
  1. 先排序,依旧设定首尾指针
  2. 于是存在:
    • 如果两指针所指元素之和小于目标值:变小一定得不到目标值,所以右指针不能左移,左指针右移
    • 如果两指针所指元素之和大于目标值:变大一定得不到目标值,所以左指针不能右移,右指针左移
  3. 遍历直到找到和为目标值的两个数
classSolution{public: vector<int>twoSum(vector<int>& price,int target){int left =0, right = price.size()-1;while(left < right){if(price[left]+ price[right]> target) right--;elseif(price[left]+ price[right]< target) left++;elsereturnvector<int>({price[left],price[right]});}returnvector<int>();}};

4. 三数之和

题目:三数之和

在这里插入图片描述

三数之和实际上是两数之和的变式:

解法一:

  • 与两数之和相同,三层循环可以枚举出想要结果,找到以后加入数组即可
    解法二:
  1. 依旧可以先排序,然后固定首尾其中一个数字,依次顺序固定,重复元素跳过
  2. 用两数之和类似双指针法,计算出三数之和为结果的数字,需要注意的是,重复元素跳过
  3. 最后遍历完成,结果加入二维数组
classSolution{public: vector<vector<int>>threeSum(vector<int>& nums){//优化sort(nums.begin(),nums.end());int n = nums.size();//利用单调性,定尾部,前面走双指针 vector<vector<int>> vv;for(int i = n-1;i >=2;i--){if( i < n-1&& nums[i]== nums[i +1]){continue;}int left =0,right = i-1;while(left < right){if(nums[left]+ nums[right]+ nums[i]>0) right--;elseif(nums[left]+ nums[right]+ nums[i]<0) left ++;else{// 找到符合条件的三元组,加入结果 vv.push_back({nums[left], nums[right], nums[i]});// 跳过左指针重复值while(left < right && nums[left]== nums[left +1])++left;// 跳过右指针重复值while(left < right && nums[right]== nums[right -1])--right;// 双指针收缩,继续找下一组++left;--right;}}}return vv;}};

5. 有效三角形的个数

题目:有效三角形的个数

实际上本题也算是三数之和的变式,只不过本体要求三数组成三角形而不是和为零:

解法一:三层循环遍历枚举
解法二:

  1. 先排序,依旧固定一边
  2. 利用三角形两小边之和大于第三边性质寻找三个符合要求的边,符合要求则计入次数
classSolution{public:inttriangleNumber(vector<int>& nums){int n = nums.size()-1;sort(nums.begin(),nums.end());int sum =0;for(int i = n; i>0; i--){int left =0,right = i-1;while(left < right){if(nums[left]+ nums[right]> nums[i]){ sum += right-left; right--;}else left++;}}return sum;}};

6. 四数之和

  • 提一点:本题需要注意的是样例里面存在超大数,所以需要注意 int 无法承载的问题

题目:四数之和

在这里插入图片描述


这里不再讲解原理,原理与三数之和类似,直接看代码:

classSolution{public: vector<vector<int>>fourSum(vector<int>& nums,int target){int n = nums.size();sort(nums.begin(),nums.end()); vector<vector<int>> vv;for(int j = n -1; j >=3;j--){if(j < n-1&& nums[j]== nums[j+1])continue;int t = target - nums[j];for(int i = j -1; i >=2;i--){if(i < j-1&& nums[i]== nums[i+1])continue;int left =0,right = i -1;while(left < right){if((longlong)nums[left]+ nums[right]+ nums[i]<(longlong)t) left++;elseif((longlong)nums[left]+ nums[right]+ nums[i]>(longlong)t) right--;else{ vv.push_back({nums[left], nums[right], nums[i],nums[j]});while(left < right && nums[left]== nums[left +1]) left++;while(left < right && nums[right]== nums[right -1]) right--; left++,right--;}}}}return vv;}};

三、快慢双指针解决环形问题

0. 算法思想:

用跑圈的问题来类比,如果一个快的人和慢的人在环形跑道跑步,如果他们一直是恒速前进,那么必定相遇,快慢双指针也类似这个问题

1. 环形链表

程序员遇到的经典环形问题是:leetcode:环形链表问题

在这里插入图片描述

采用快慢指针的思想,如果有环就必定能相遇,而如果没环必定能越界

/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */classSolution{public:boolhasCycle(ListNode *head){typedefstructListNode ListNode; ListNode* fast = head; ListNode* slow = head;while(fast!=NULL&& fast->next!=NULL){ fast = fast->next->next; slow = slow->next;if(slow == fast){returntrue;}}returnfalse;}};

2. 快乐数

题目:leetcode:快乐数

在这里插入图片描述

本题的两种情况其实可以看作是一种情况:

  • 有环:环内无1,成一个大环
  • 有环:环内只有1,可看作一循环,即为快乐数
    所以只需要判断快慢指针相遇的时候他们所指元素的值是不是1,即可判断是否是快乐数
classSolution{public:intget_sosq(int& n)//获取平方和{int sum =0;while(n){ sum +=pow((n %10),2); n /=10;}return sum;}boolisHappy(int n){int show = n, fast = n; show =get_sosq(show); fast =get_sosq(fast); fast =get_sosq(fast);while(show != fast){ show =get_sosq(show); fast =get_sosq(fast); fast =get_sosq(fast);}if(fast ==1)returntrue;returnfalse;}};

  • 本节完…

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk