在C++编程中,双指针算法是一种高效的解题思路,其核心是通过设置两个指针(或索引)遍历数据结构(如数组、链表、字符串等),利用指针的移动规则减少无效操作,从而将时间复杂度从暴力解法的O(n²)优化至O(n)或O(n log n)。这种算法广泛应用于链表操作、数组处理、字符串匹配等场景。

一、双指针算法的核心思想

双指针算法的本质是通过两个指针的协同移动,缩小问题的处理范围。与暴力解法中嵌套循环的“盲目遍历”不同,双指针的移动具有明确的逻辑(如基于数据的有序性、结构特性等),从而避免冗余计算。

其核心优势体现在:

  • 时间优化:将多层循环转化为单层遍历,降低时间复杂度;
  • 空间优化:多数情况下无需额外空间(原地操作),空间复杂度可保持O(1);
  • 逻辑清晰:通过指针的移动规则直观反映问题的解决思路。

二、双指针的常见类型及应用场景

根据指针的移动方向和作用,双指针可分为三大类:快慢指针左右指针同向指针。以下结合具体场景详细讲解。

(一)快慢指针(Floyd’s Tortoise and Hare)

快慢指针指两个指针以不同速度遍历数据结构(如链表中,快指针每次走2步,慢指针每次走1步)。其核心应用是处理链表中的环问题查找特定位置(如中间节点)。

1. 链表环检测(LeetCode 141)

问题:判断一个单链表是否存在环。
暴力解法:用哈希表记录访问过的节点,若重复访问则有环,时间O(n)但空间O(n)。
双指针解法

  • 设快指针fast和慢指针slow,初始均指向头节点;
  • fast每次移动2步,slow每次移动1步;
  • 若链表有环,fast会先进入环并绕环移动,最终与slow在环内相遇;
  • fast到达nullptr,则无环。

代码实现

struct ListNode {int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {}
};bool hasCycle(ListNode *head) {if (head == nullptr || head->next == nullptr) return false;ListNode *slow = head;ListNode *fast = head->next; // 初始错开,避免直接相遇while (slow != fast) {if (fast == nullptr || fast->next == nullptr) return false;slow = slow->next;       // 慢指针走1步fast = fast->next->next; // 快指针走2步}return true;
}

时间复杂度:O(n)(无环时fast走n/2步;有环时相遇前slow最多走n步)。
空间复杂度:O(1)(仅用两个指针)。

2. 寻找环的入口(LeetCode 142)

问题:若链表有环,找到环的入口节点。
算法思路

  1. 先用快慢指针判断有环,记录相遇点meet
  2. slow从头节点出发,fastmeet出发,两者均每次走1步;
  3. 两指针再次相遇的节点即为环的入口。

原理:设头节点到入口距离为a,入口到相遇点距离为b,环长为c。则相遇时:

  • slow走了a + b
  • fast走了a + b + k*c(绕环k圈);
  • fast速度是slow的2倍,故2*(a + b) = a + b + k*ca = k*c - b b=k*c-a
  • 因此,slow从头部走a步,与fastmeetk*c - b步(等价于绕环k圈后回到入口)相遇。

重置slow = 0,fast仍在meet处(等价于初始走了a+b),当slow=a(slow走了a步),fast=a+b+kc-b=a+kc,所以a,b在环的入口相遇
在这里插入图片描述

代码实现

ListNode *detectCycle(ListNode *head) {if (head == nullptr) return nullptr;ListNode *slow = head, *fast = head;// 第一步:找到相遇点do {if (fast->next == nullptr || fast->next->next == nullptr) return nullptr;slow = slow->next;fast = fast->next->next;} while (slow != fast);// 第二步:寻找入口slow = head;while (slow != fast) {slow = slow->next;fast = fast->next;}return fast;
}
3. 寻找链表的中间节点(LeetCode 876)

问题:返回单链表的中间节点(若长度为偶数,返回第二个中间节点)。
算法思路

  • 快指针每次走2步,慢指针每次走1步;
  • fast到达尾节点时,slow恰好指向中间节点。

代码实现

ListNode* middleNode(ListNode* head) {ListNode *slow = head, *fast = head;while (fast != nullptr && fast->next != nullptr) {slow = slow->next;fast = fast->next->next;}return slow;
}

(二)左右指针(相向指针)

左右指针指两个指针分别从数据结构的两端出发,相向而行(左指针从左向右,右指针从右向左),多用于有序数组/字符串的处理。其核心是利用数据的有序性,通过指针移动排除无效解。

1. 两数之和(有序数组版,LeetCode 167)

问题:给定有序数组nums和目标值target,找到两个数使得和为target,返回索引(下标从1开始)。
暴力解法:嵌套循环遍历所有组合,时间O(n²)。
双指针解法

  • 左指针left初始指向0,右指针right指向n-1
  • 计算当前和sum = nums[left] + nums[right]
    • sum == target,返回[left+1, right+1]
    • sum > target,说明右指针太大,right--
    • sum < target,说明左指针太小,left++

原理:数组有序保证了指针移动的有效性——当sum > target时,减小右指针可降低总和;当sum < target时,增大左指针可提高总和,无需检查其他组合。

代码实现

vector<int> twoSum(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left < right) {int sum = nums[left] + nums[right];if (sum == target) {return {left + 1, right + 1};} else if (sum > target) {right--;} else {left++;}}return {-1, -1}; // 题目保证有解,此行为冗余
}

时间复杂度:O(n),空间复杂度O(1)。

2. 反转字符串(LeetCode 344)

问题:原地反转字符串(如["h","e","l","l","o"]["o","l","l","e","h"])。
算法思路

  • 左指针left指向0,右指针right指向n-1
  • 交换nums[left]nums[right],然后left++right--,直到left >= right

代码实现

void reverseString(vector<char>& s) {int left = 0, right = s.size() - 1;while (left < right) {swap(s[left], s[right]);left++;right--;}
}
3. 二分查找(本质是左右指针的变体)

二分查找中,leftright指针分别指向搜索范围的两端,通过比较中间值与目标值,不断缩小范围,本质是左右指针的“跳跃式移动”。

代码实现

int search(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2; // 避免溢出if (nums[mid] == target) return mid;else if (nums[mid] < target) left = mid + 1;else right = mid - 1;}return -1;
}

(三)同向指针(一前一后指针)

同向指针指两个指针从同一端出发,沿相同方向移动(通常一个在前“探索”,一个在后“记录”),多用于数组去重、子数组/子串问题

1. 删除有序数组中的重复项(LeetCode 26)

问题:原地删除有序数组中的重复元素,返回新长度(如[1,1,2]→长度2,数组变为[1,2])。
算法思路

  • 慢指针slow记录有效元素的尾位置(初始0);
  • 快指针fast遍历数组(初始1);
  • nums[fast] != nums[slow],说明找到新元素,slow++并将nums[fast]复制到nums[slow]
  • 最终slow + 1为新长度。

代码实现

int removeDuplicates(vector<int>& nums) {if (nums.empty()) return 0;int slow = 0;for (int fast = 1; fast < nums.size(); fast++) {if (nums[fast] != nums[slow]) {slow++;nums[slow] = nums[fast];}}return slow + 1;
}
2. 移动零(LeetCode 283)

问题:将数组中的0移到末尾,保持非零元素的相对顺序(如[0,1,0,3,12][1,3,12,0,0])。
算法思路

  • 慢指针slow记录非零元素的尾位置(初始0);
  • 快指针fast遍历数组,若nums[fast] != 0,则交换nums[slow]nums[fast]slow++
  • 遍历结束后,slow及之后的位置全部赋0。

代码实现

void moveZeroes(vector<int>& nums) {int slow = 0;for (int fast = 0; fast < nums.size(); fast++) {if (nums[fast] != 0) {swap(nums[slow], nums[fast]);slow++;}}// 剩余位置补0(可选,因原数组0已被交换到后面)for (int i = slow; i < nums.size(); i++) {nums[i] = 0;}
}
3. 滑动窗口(同向指针的高级应用)

滑动窗口是同向指针的典型场景,用于解决子数组/子串的最值问题(如最长、最短、包含特定元素等)。其核心是用leftright指针维护一个“窗口”,通过移动right扩张窗口,移动left收缩窗口,在O(n)时间内找到最优解。

示例:长度最小的子数组(LeetCode 209)
问题:给定数组nums和目标值s,找到和≥s的最短子数组长度(若无则返回0)。

算法思路

  • leftright初始均为0,sum记录窗口内元素和;
  • 移动right,将nums[right]加入sum
  • sum ≥ s时,尝试移动left缩小窗口,更新最小长度;
  • 重复直至right遍历结束。

代码实现

int minSubArrayLen(int s, vector<int>& nums) {int n = nums.size();int min_len = INT_MAX;int left = 0, sum = 0;for (int right = 0; right < n; right++) {sum += nums[right];// 收缩窗口while (sum >= s) {min_len = min(min_len, right - left + 1);sum -= nums[left];left++;}}return min_len == INT_MAX ? 0 : min_len;
}

时间复杂度:O(n)(每个元素被rightleft各访问一次)。

三、双指针算法的进阶技巧

  1. 指针初始化的灵活性
    快慢指针初始可不同步(如环检测中fastslow超前一步);左右指针可从非端点出发(如处理子数组时限制窗口范围)。

  2. 结合数据结构特性
    有序数组优先考虑左右指针;链表问题优先考虑快慢指针;子串问题优先考虑滑动窗口(同向指针)。

  3. 多指针扩展
    某些场景需3个指针(如荷兰国旗问题:用leftmidright划分0、1、2),核心思想与双指针一致。

  4. 边界条件处理
    需注意指针越界(如链表fast->next是否为nullptr)、空数据结构(如数组长度为0)等特殊情况。


双指针算法是C++编程中优化效率的核心思想之一,其核心在于通过指针的协同移动减少无效遍历。根据应用场景可分为快慢指针(链表环、中间节点)、左右指针(有序数组、反转)、同向指针(去重、滑动窗口)三大类,每种类型均通过明确的移动规则将时间复杂度从O(n²)降至O(n)。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【LLM】GLM-4.5模型架构和原理

note 文章目录note一、GLM-4.5模型二、Slime RL强化学习训练架构Reference一、GLM-4.5模型 大模型进展&#xff0c;GLM-4.5技术报告,https://arxiv.org/pdf/2508.06471&#xff0c;https://github.com/zai-org/GLM-4.5&#xff0c;包括GLM-4.5&#xff08;355B总参数&#xff…

LLM 中增量解码与模型推理解读

在【LLM】LLM 中 token 简介与 bert 实操解读一文中对 LLM 基础定义进行了介绍&#xff0c;本文会对 LLM 中增量解码与模型推理进行解读。 一、LLM 中增量解码定义 增量解码&#xff08;Incremental Decoding&#xff09;是指在自回归文本生成过程中&#xff0c;模型每次只计…

1.Spring Boot:超越配置地狱,重塑Java开发体验

目录 一、Spring框架&#xff1a;伟大的基石 历史背景与挑战 Spring的革命性贡献 新的挑战&#xff1a;配置地狱 二、Spring Boot&#xff1a;约定大于配置的革命 四大核心特性 1. 快速创建独立应用 2. 自动配置&#xff1a;智能化的魔法 3. 起步依赖&#xff1a;依赖管…

assert使用方法

assert 是 Python 中用来进行 调试 和 验证 的一个关键字&#xff0c;它用于测试一个 条件表达式 是否为真。如果条件为假&#xff0c;assert 会抛出一个 AssertionError 异常&#xff0c;通常带有错误信息。语法&#xff1a;assert condition, "Error message"condi…

【实习总结】快速上手Git:关键命令整理

目录 git的四大工作区域 git首次配置 克隆远程仓库 提交代码到远程仓库 查看文件状态&#xff08;可选&#xff09; 添加文件到暂存区 将暂存区的内容提交到本地仓库 将本地的提交上传到远程仓库 拉取并合并代码 第一种方式 第二种方式 分支管理 查看与创建分支 …

02-开发环境搭建与工具链

第2课&#xff1a;开发环境搭建与工具链 &#x1f4da; 课程目标 掌握DevEco Studio的下载、安装和配置熟悉HMS Core&#xff08;华为移动服务&#xff09;的使用了解鸿蒙模拟器与真机调试环境掌握必备开发工具的使用 &#x1f6e0;️ DevEco Studio环境搭建 2.1 下载与安装…

删掉一个元素以后全为1的最长子数组-滑动窗口

1493. 删掉一个元素以后全为 1 的最长子数组 - 力扣&#xff08;LeetCode&#xff09; Solution #include<iostream> #include<vector> using namespace std;class Solution { public://滑动窗口//动态维护一个窗口&#xff0c;窗口内只能有1个0&#xff0c;记录窗…

【计算机网络 | 第8篇】编码与调制

文章目录通信系统中的编码与调制&#xff1a;从信道基础到信号传输技术一、信道与通信电路&#x1f342;二、三种基本通信方式&#x1f4d6;1. 单向通信&#xff08;单工通信&#xff09;2. 双向交替通信&#xff08;半双工通信&#xff09;3. 双向同时通信&#xff08;全双工通…

当AI遇上终端:Gemini CLI的技术魔法与架构奥秘

"代码不仅仅是指令的集合&#xff0c;更是思想的载体。当AI与终端相遇&#xff0c;会碰撞出怎样的火花&#xff1f;" 在这个AI技术日新月异的时代&#xff0c;Google推出的Gemini CLI无疑是一颗璀璨的明星。它不仅仅是一个命令行工具&#xff0c;更是一个将人工智能无…

ViLU: Learning Vision-Language Uncertainties for Failure Prediction

研究方向&#xff1a;Image Captioning1. 论文介绍本文提出ViLU&#xff08;Vision-Language Uncertainties&#xff09;&#xff0c;一个用于学习视觉语言不确定性量化&#xff08;UQ&#xff09;和检测视觉语言模型故障的事后框架。使用VLMs进行量化&#xff08;UQ&#xff0…

数据集笔记:百度地图高德地图坐标互转

1 为什么会有高德坐标系和百度坐标系&#xff1f;根据《测绘法》和国家保密法规&#xff0c;在中国大陆范围内的地理坐标数据必须做加密处理&#xff0c;不允许直接使用 WGS84&#xff08;openstreetmap&#xff09;所以出现了GCJ-02 和 BD-09高德、腾讯、谷歌中国都遵循 GCJ-0…

SkyWalking高效线程上下文管理机制:确保调用链中traceId来自同一个请求

SkyWalking Agent 能确保获取到“正确”的 traceId,其核心在于它建立并维护了一套高效的线程上下文管理机制。这套机制确保了即使在复杂的多线程、异步环境下,也能将正确的上下文(包含 traceId)与当前正在执行的代码逻辑关联起来。 其工作原理可以概括为下图所示的流程: …

Kafka-Eagle安装

目录Eagle环境安装Mysql环境准备Kafka环境准备Eagle安装Kafka-Eagle框架可以监控Kafka集群的整体运行情况&#xff0c;在生产环境中经常使用 Eagle环境安装 Mysql环境准备 Eagle的安装依赖于Mysql&#xff0c;Mysql主要用来存储可视化展示的数据 将mysql文件夹及里面所有内…

Matlab系列(005) 一 归一化

目录1、前言2、什么是归一化&#xff1f;3、为什么要进行归一化4、归一化方法详解与Matlab实现5、总结1、前言 ​   归一化技术是数据预处理的核心环节&#xff0c;本文将深度解析主流归一化方法&#xff0c;提供可复现Matlab代码&#xff0c;并探讨其在各领域中的应用场景。…

【K8s】整体认识K8s之namespace

命名空间将资源划分为相互隔离的组。kubectl get namespace/ns系统默认创建四个namespace&#xff0c;分别是default、kube-node-lease、kube-public、kube-system。default 没有指明使用其它命名空间的对象所使用的默认命名空间、kube-system 系统创建对象所使用的命名空间。…

rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(十八) 使用表格

使用表格egui_extras::TableBuilder // Cargo.toml [dependencies] eframe "0.32.1" egui "0.32.1" egui_extras "0.32.1"egui_extras::Column::auto() 列宽根据内容自动计算.resizable(true) 允许用户手动拖动调整列宽 fn main() -> efra…

【C#】构造函数实用场景总结

文章目录前言一、构造函数是什么&#xff1f;二、构造函数的用法1.初始化对象&#xff0c;避免无效状态2 初始化静态成员3 构造函数重载4.构造函数链5. 单例模式&#xff0c;多次实例化保持一个对象6. 依赖注入7. 初始化只读对象前言 构造函数是我们平常编程里经常能碰到的老伙…

LLM预训练架构全解析:从零构建一个语言世界的“操作系统”

导读&#xff1a;作为开发者&#xff0c;我们每天都在import或#include各种库&#xff0c;我们信任这些由无数代码构成的底层依赖。那么&#xff0c;当我们调用一个LLM时&#xff0c;它所依赖的那个更底层的、无形的**“语言操作系统”**&#xff0c;又是如何被“编译”出来的&…

Linux服务测试题(DNS,NFS,DHCP,HTTP)

一&#xff0c;实验拓扑&#xff1a;二&#xff0c;需求APPSRV&#xff1a;主机名&#xff1a;appsrv.example.comip地址&#xff1a;192.168.100.10网关&#xff1a;192.168.100.254网卡为NAT模式STORAGESRV&#xff1a;主机名&#xff1a;storagesrv.example.comip地址&#…

DevOps 简介及就业前景

DevOps 简介及就业前景 目录 DevOps简介核心概念重难点解析具体场景使用就业前景学习路径最佳实践 DevOps简介 什么是DevOps DevOps是Development&#xff08;开发&#xff09;和Operations&#xff08;运维&#xff09;的组合词&#xff0c;是一种软件开发和IT运维的文化…