双指针算法详解:从原理到实战(含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

Flutter for OpenHarmony:injector 轻量级依赖注入库(比 GetIt 更简单的选择) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:injector 轻量级依赖注入库(比 GetIt 更简单的选择) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 依赖注入(Dependency Injection, DI)是解耦架构的核心。 在 Flutter 社区,get_it 是当之无愧的霸主,但有时候我们想要一个更简单、没有 Service Locator 模式那种“全局单例”味道的库,或者需要一个支持模块化注入的方案。 injector 是一个非常轻量的 DI 库。它不使用代码生成,提供基于构建器(Builder)的依赖注册机制。 对于 OpenHarmony 开发者,使用 DI 库可以将鸿蒙特定的实现(如 OhosPermissionService)与通用业务逻辑解耦,实现一套代码,多端运行。 一、核心原理 injector 的工作原理非常纯粹:它维护了一个 Map,

By Ne0inhk
Flutter for OpenHarmony:Flutter 三方库 gql_link — 掌握鸿蒙端 GraphQL 请求拦截与扩展核心(适配鸿蒙 HarmonyOS Next ohos)

Flutter for OpenHarmony:Flutter 三方库 gql_link — 掌握鸿蒙端 GraphQL 请求拦截与扩展核心(适配鸿蒙 HarmonyOS Next ohos)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net。 Flutter for OpenHarmony:Flutter 三方库 gql_link — 掌握鸿蒙端 GraphQL 请求拦截与扩展核心(适配鸿蒙 HarmonyOS Next ohos) 在现代 App 开发中,GraphQL 的灵活性让我们能精准获取数据。然而,一个健壮的 GraphQL 架构不仅需要发送请求,更需要对请求进行“手术刀”级的拦截、转换和链路编排。例如:统一注入身份 Token、自动日志记录、根据网络状况切换端点等。 在 Flutter for OpenHarmony 开发中,gql_link 库就是这套架构的灵魂所在。它定义了抽象的 Link 通信契约,让我们能像插拔积木一样组合不同的通信能力。今天,

By Ne0inhk
Kali Linux下载安装及配置(VMware Workstation虚拟机下载安装)保姆级图文教程(持续更新)(2026/3/5最新更新)

Kali Linux下载安装及配置(VMware Workstation虚拟机下载安装)保姆级图文教程(持续更新)(2026/3/5最新更新)

目录 环境介绍 ISO镜像安装 一、VMware Workstation17 Pro安装  二、 kali下载 三、kali安装 温馨提醒: 四、基础配置 1.开机 2.联网与时区设置 一、联网(无法联网状况查看此条) 二、改时区 3.更新 一.更换源(建议不用,除非更新时报错) 编辑二.更新(建议忽略第一步,直接这一步) 报错及解决 4.汉化 5.中文输入法安装 一.安装fcitx 二.安装中文输入法 谨防抄袭文章,注意不要被卖课的骗了 前置提醒:信息技术更新速度较快,本文时效性可能不足,可能出现落后消息,请认真理性看待,如有遗漏、

By Ne0inhk

OpenClaw 系统架构深度解析

文章目录 * OpenClaw 系统架构深度解析 * 🏗️ 一、架构概览与设计哲学 * 1.1 核心设计原则 * 1.2 整体架构图 * 🔧 二、核心层深度剖析 * 2.1 感知引擎架构 * 2.2 规划引擎架构 * 2.3 执行引擎架构 * 2.4 记忆引擎架构 * 🌐 三、编排层架构 * 3.1 工作流引擎 * 3.2 服务网格与通信 * 📊 四、数据流与状态管理 * 4.1 数据流架构 * 4.2 状态管理架构 * 🔐 五、安全架构 * 5.1 安全架构设计 * 📈 六、可观测性架构 * 6.1

By Ne0inhk