当前位置: 首页 > news >正文

优选算法之双指针:从原理到实战,解决数组与链表

目录

一、双指针算法核心分类

1. 对撞指针:从两端向中间逼近

2. 快慢指针:一快一慢检测循环

二、对撞指针经典例题实战

例题 1:移动零(LeetCode 283)—— 数组区间划分

题目描述

算法思路

代码实现(C++)

复杂度分析

例题 2:复写零(LeetCode 1089)—— 逆序处理避免覆盖

题目描述

算法思路

代码实现(C++)

例题 3:两数之和(LeetCode 167)—— 有序数组目标值查找

题目描述

算法思路

代码实现(C++)

例题 4:有效三角形个数(LeetCode 611)—— 固定最长边优化

题目描述

算法思路

代码实现(C++)

例题 5:三数之和(LeetCode 15)—— 固定一端 + 去重

题目描述

算法思路

代码实现(C++)

三、快慢指针经典例题实战

例题:快乐数(LeetCode 202)—— 循环检测

题目描述

算法思路

代码实现(C++)

原理补充

四、双指针算法总结

1. 核心优势

2. 适用场景速查表

3. 避坑指南


双指针是算法领域中高效且灵活的技巧,核心是通过两个指针在数据结构(如数组、链表)上协同移动,将原本 O (n²) 复杂度的问题优化到 O (n),尤其适用于处理 “区间划分”“目标值查找”“循环检测” 等场景。接下来我们将系统拆解双指针的两种核心形式(对撞指针、快慢指针),结合经典例题详解原理与实现,彻底掌握这一算法利器。

一、双指针算法核心分类

双指针并非单一算法,而是根据指针移动方向和场景,分为对撞指针快慢指针两类,二者适用场景完全不同,需精准区分。

1. 对撞指针:从两端向中间逼近

  • 核心逻辑:两个指针分别从数据结构的左端(left) 和右端(right) 出发,根据条件向中间移动,直到指针相遇或错开,常用于有序数组 / 字符串的区间查找。
  • 终止条件
    1. 指针相遇(left == right);
    2. 指针错开(left > right)。
  • 适用场景:两数之和、三数之和、有效三角形个数、盛水最多的容器等。

2. 快慢指针:一快一慢检测循环

  • 核心逻辑:两个指针从同一端出发,移动速度不同(通常慢指针走 1 步,快指针走 2 步),利用 “速度差” 检测数据结构中的循环特性,或定位特定位置(如链表中点)。
  • 核心优势:无需额外空间(如哈希表),即可判断循环,空间复杂度优化至 O (1)。
  • 适用场景:快乐数、环形链表检测、链表中点查找等。

二、对撞指针经典例题实战

对撞指针的关键是利用数据的有序性(若数据无序,需先排序),通过移动指针缩小查找范围,减少无效枚举。以下是 5 道必刷例题,覆盖从简单到复杂的应用场景。

例题 1:移动零(LeetCode 283)—— 数组区间划分

题目描述

给定数组 nums,将所有 0 移动到数组末尾,同时保持非零元素的相对顺序,要求原地操作

  • 示例:输入 [0,1,0,3,12],输出 [1,3,12,0,0]
算法思路

本质是 “数组分两块”:用 cur 指针遍历数组,dest 指针记录 “非零元素区间的末尾”,最终让 [0, dest] 全为非零元素,[dest+1, n-1] 全为 0。

  1. 初始化 cur = 0(遍历指针),dest = -1(非零区间末尾,初始无元素);
  2. cur 遍历数组:
    • 若 nums[cur] != 0dest 先右移(扩展非零区间),交换 nums[dest] 与 nums[cur],确保非零元素进入左侧区间;
    • 若 nums[cur] == 0:直接跳过,让 0 留在右侧。
代码实现(C++)

class Solution {
public:void moveZeroes(vector<int>& nums) {// cur:遍历指针,dest:非零元素区间末尾for (int cur = 0, dest = -1; cur < nums.size(); cur++) {if (nums[cur] != 0) { // 遇到非零元素,移入左侧区间dest++;swap(nums[dest], nums[cur]);}}}
};
复杂度分析
  • 时间复杂度:O (n),cur 遍历数组一次;
  • 空间复杂度:O (1),仅用两个指针,原地操作。

例题 2:复写零(LeetCode 1089)—— 逆序处理避免覆盖

题目描述

给定固定长度的数组 arr,将每个 0 复写一遍(即 0→0,0),其余元素右移,要求不超过数组长度且原地操作。

  • 示例:输入 [1,0,2,3,0,4,5,0],输出 [1,0,0,2,3,0,0,4]
算法思路

若从左向右复写,0 会覆盖未处理的元素,因此需先找最后一个复写的位置,再从右向左复写:

  1. 第一阶段(找最后位置):用 cur 遍历数组,dest 模拟复写后的位置(遇 0 加 2,非 0 加 1),直到 dest 超出数组长度;
  2. 第二阶段(处理边界):若 dest 恰好等于数组长度(说明最后一个元素是 0,复写会超出),需单独将数组最后一位设为 0,再调整 cur 和 dest
  3. 第三阶段(逆序复写):从 cur 向 0 移动,遇 0 则 dest 位置和 dest-1 位置都设为 0,遇非 0 则 dest 位置设为该元素,逐步向左复写。
代码实现(C++)

class Solution {
public:void duplicateZeros(vector<int>& arr) {int n = arr.size();int cur = 0, dest = -1;// 1. 找最后一个复写的元素while (cur < n) {if (arr[cur] == 0) dest += 2;else dest += 1;if (dest >= n - 1) break; // 已覆盖数组末尾,停止cur++;}// 2. 处理边界:dest超出数组长度(最后一个元素是0)if (dest == n) {arr[n - 1] = 0;cur--;dest -= 2;}// 3. 从后向前复写while (cur >= 0) {if (arr[cur] == 0) {arr[dest--] = 0;arr[dest--] = 0;} else {arr[dest--] = arr[cur];}cur--;}}
};

例题 3:两数之和(LeetCode 167)—— 有序数组目标值查找

题目描述

给定升序排列的数组 nums 和目标值 target,找出两个数使其和为 target,返回这两个数(或下标)。

  • 示例:输入 [2,7,11,15]target=9,输出 [2,7]
算法思路

利用数组有序性,用对撞指针缩小范围:

  1. 初始化 left=0(左端),right=n-1(右端);
  2. 计算 sum = nums[left] + nums[right]
    • 若 sum == target:找到结果,返回;
    • 若 sum < target:左指针右移(需更大的数);
    • 若 sum > target:右指针左移(需更小的数)。
代码实现(C++)

class Solution {
public: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 {nums[left], nums[right]}; // 返回数值(若需下标则返回left+1, right+1)} else if (sum < target) {left++;} else {right--;}}return {-1}; // 无结果(题目保证有解可省略)}
};

例题 4:有效三角形个数(LeetCode 611)—— 固定最长边优化

题目描述

给定非负整数数组 nums,返回能组成三角形的三元组个数(需满足:任意两边之和大于第三边,即最小两边之和大于最大边)。

  • 示例:输入 [2,2,3,4],输出 3(有效组合:(2,3,4)、(2,3,4)、(2,2,3))。
算法思路

先排序数组,固定最长边(从后向前枚举),再用对撞指针找满足条件的最小两边:

  1. 排序数组(升序);
  2. 固定最长边 nums[i]i 从 n-1 向 2 移动,因三角形需 3 条边);
  3. 初始化 left=0right=i-1(在 [0, i-1] 中找两数之和大于 nums[i]):
    • 若 nums[left] + nums[right] > nums[i]right 与 [left, right-1] 中所有元素都能组成三角形,计数 right-leftright 左移;
    • 若 nums[left] + nums[right] <= nums[i]left 右移(需更大的数)。
代码实现(C++)

class Solution {
public:int triangleNumber(vector<int>& nums) {sort(nums.begin(), nums.end());int n = nums.size();int ret = 0;// 固定最长边 nums[i]for (int i = n - 1; i >= 2; i--) {int left = 0, right = i - 1;while (left < right) {if (nums[left] + nums[right] > nums[i]) {ret += right - left; // 所有[left, right-1]都满足right--;} else {left++;}}}return ret;}
};

例题 5:三数之和(LeetCode 15)—— 固定一端 + 去重

题目描述

给定数组 nums,找出所有和为 0 且不重复的三元组(i≠j≠k)。

  • 示例:输入 [-1,0,1,2,-1,-4],输出 [[-1,-1,2],[-1,0,1]]
算法思路

排序后固定一个数,再用对撞指针找另外两个数,核心是去重(避免重复三元组):

  1. 排序数组(升序,便于去重和缩小范围);
  2. 固定 nums[i](若 nums[i] > 0,则三数和必大于 0,直接 break);
  3. 初始化 left=i+1right=n-1,目标值 target = -nums[i]
    • 若 nums[left] + nums[right] == target:记录三元组,再分别移动 left 和 right,并跳过重复元素;
    • 若和小于 targetleft 右移;
    • 若和大于 targetright 左移;
  4. 固定数 nums[i] 也需去重(跳过与前一个元素相同的值)。
代码实现(C++)

class Solution {
public:vector<vector<int>> threeSum(vector<int>& nums) {vector<vector<int>> res;sort(nums.begin(), nums.end());int n = nums.size();for (int i = 0; i < n; ) {if (nums[i] > 0) break; // 三数和必为正,终止int left = i + 1, right = n - 1;int target = -nums[i];while (left < right) {int sum = nums[left] + nums[right];if (sum < target) {left++;} else if (sum > target) {right--;} else {// 记录结果res.push_back({nums[i], nums[left], nums[right]});left++;right--;// 去重:跳过left的重复元素while (left < right && nums[left] == nums[left - 1]) left++;// 去重:跳过right的重复元素while (left < right && nums[right] == nums[right + 1]) right--;}}// 去重:跳过i的重复元素i++;while (i < n && nums[i] == nums[i - 1]) i++;}return res;}
};

三、快慢指针经典例题实战

快慢指针的核心是利用速度差检测循环,无需额外空间即可判断 “是否进入循环” 及 “循环起点”,典型应用是 “快乐数” 和 “环形链表”。

例题:快乐数(LeetCode 202)—— 循环检测

题目描述

“快乐数” 定义:对于正整数 n,每次将其替换为各位数字的平方和,重复此过程,若最终变为 1 则是快乐数,否则进入无限循环(非快乐数)。

  • 示例:输入 n=19,输出 true(19→82→68→100→1);输入 n=2,输出 false
算法思路

问题本质是判断 “平方和过程是否进入循环”:

  1. 定义 Jisuan 函数:计算一个数的各位平方和;
  2. 初始化 slow = n(慢指针,每次走 1 步:计算 1 次平方和),fast = Jisuan(n)(快指针,每次走 2 步:计算 2 次平方和);
  3. 若 slow == fast:说明进入循环,若循环点是 1 则为快乐数,否则不是。
代码实现(C++)

class Solution {
public:// 计算n的各位平方和int Jisuan(int n) {int sum = 0;while (n > 0) {int digit = n % 10; // 取个位sum += digit * digit;n /= 10; // 去掉个位}return sum;}bool isHappy(int n) {int slow = n;int fast = Jisuan(n); // 快指针先出发一步// 若slow != fast,继续移动while (slow != fast) {slow = Jisuan(slow); // 慢指针走1步fast = Jisuan(Jisuan(fast)); // 快指针走2步}// 循环终止:若相遇点是1则为快乐数return fast == 1;}
};
原理补充
  • 为何会进入循环?根据 “鸽巢原理”,平方和的范围始终在 [1, 810](最大 999999999 的平方和为 9²×9=729),最多 811 次计算必重复,即进入循环;
  • 为何快慢指针能相遇?若有循环,快指针最终会追上慢指针(类似环形跑道上的快、慢运动员)。

四、双指针算法总结

1. 核心优势

  • 时间优化:将暴力枚举的 O (n²) 降至 O (n) 或 O (n log n)(排序耗时);
  • 空间优化:无需额外数据结构(如哈希表),空间复杂度多为 O (1)。

2. 适用场景速查表

指针类型核心场景典型题目
对撞指针有序数组查找、区间划分两数之和、三数之和、有效三角形个数
快慢指针循环检测、位置定位快乐数、环形链表、链表中点

3. 避坑指南

  • 对撞指针:若数据无序,需先排序(如三数之和),否则无法缩小范围;
  • 快慢指针:初始化时快指针可先移动一步(如快乐数),避免初始状态 slow == fast
  • 去重处理:涉及 “不重复结果” 的题目(如三数之和),需在指针移动后跳过重复元素,避免冗余。

双指针算法是解决数组、链表问题的 “瑞士军刀”,掌握其核心逻辑后,可轻松应对多数中等难度的算法题。建议结合本文例题反复练习,重点体会 “指针移动的条件” 和 “边界处理”,逐步形成 “见题思指针” 的解题直觉。

http://www.dtcms.com/a/488375.html

相关文章:

  • 泰安网站建设企业门户网站静态模板
  • 做招聘网站的背景图片手机建网站 优帮云
  • 湖州网站建设湖州网站建设网站建设运营计划书
  • Spring的核心思想与注解
  • 洛阳响应式建站wordpress无法搜索插件
  • 幻灯片在什么网站做dw做的静态网站怎么分享链接
  • 做网站如何推销小公司做网站的好处
  • 力扣Hot100--106.对称二叉树
  • 胡恩全10.15作业
  • 网站推广需要多少钱教学网站开发源码
  • ACSM-CPT 8周冲刺每日学习计划(10/16–12/15)
  • 公司备案网站名称代理ip注册网站都通不过
  • 杭州建设银行网站智慧团建登录不上
  • 大型商城网站开发wordpress侧边栏加图片
  • Linux内核架构浅谈37-深入理解Linux页帧标志:从PG_locked到PG_dirty的核心原理与实践
  • 建设网站的功能及目的是什么wordpress好用的地图
  • 佛山找企业的网站WORDPRESS添加前台会员注册
  • 【完整源码+数据集+部署教程】 【零售和消费品&存货】条形码检测系统源码&数据集全套:改进yolo11-TADDH
  • 上海建网站公司排名常用的设计软件有哪些
  • 10、一个简易 vector:C++ 模板与 STL
  • 营销网站设计网站获取访客qq 原理
  • Aosp14桌面壁纸和锁屏壁纸的设置和加载分析
  • 作业1.1
  • 减肥养生网站建设360极速浏览器网站开发缓存
  • 网站开发人员资质北京汽车网站建设
  • 网站模板修改教程活动推广文案
  • 专门做旅行用品的网站沈阳市浑南区城乡建设局网站
  • 哪一个景区网站做的最成熟wordpress loop count
  • JDK高版本特性总结与ZGC实践
  • 搜索优化整站优化在线阅读网站开发教程