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

双指针算法技巧

双指针 - 力扣(LeetCode)全球极客挚爱的技术成长平台

双指针技巧秒杀七道链表题目 | labuladong 的算法笔记

双指针技巧秒杀七道数组题目 | labuladong 的算法笔记

双指针算法(Two Pointers)详解与 Go 实现

一、什么是双指针算法?

双指针算法是一种通过在数据结构(如数组、链表)中设置两个指针(索引),通过移动指针高效解决问题的技巧。核心思想是减少冗余遍历,将原本需要嵌套循环(O (n²))的问题优化为线性时间(O (n))。

根据指针移动方向,可分为两类:

  • 相向指针:两个指针从两端向中间移动(如反转、回文判断)。

  • 同向指针:两个指针从同一端出发,快慢指针(如链表环检测)或前后指针(如移除元素)。

二、经典场景与 Go 实现示例
1. 两数之和(有序数组)

问题:给定有序数组 nums 和目标值 target,找到两个数使和为 target,返回索引(LeetCode 167)。 思路:左指针(left)从起点,右指针(right)从终点,根据当前和与 target 的大小移动指针。

go

func twoSum(numbers []int, target int) []int {left, right := 0, len(numbers)-1for left < right {sum := numbers[left] + numbers[right]if sum == target {return []int{left + 1, right + 1} // 题目要求1-based索引} else if sum < target {left++ // 和太小,左指针右移(增大 sum)} else {right-- // 和太大,右指针左移(减小 sum)}}return []int{} // 题目保证有解,实际不会走到这
}
2. 三数之和

问题:找到数组中所有和为 0 的三元组,不重复(LeetCode 15)。 思路:排序后固定一个数,用双指针找另外两个数,注意去重。

go

import "sort"
​
func threeSum(nums []int) [][]int {sort.Ints(nums)res := [][]int{}n := len(nums)for i := 0; i < n-2; i++ {// 去重:跳过重复的固定值if i > 0 && nums[i] == nums[i-1] {continue}left, right := i+1, n-1for left < right {sum := nums[i] + nums[left] + nums[right]if sum == 0 {res = append(res, []int{nums[i], nums[left], nums[right]})// 去重:跳过重复的左/右值for left < right && nums[left] == nums[left+1] {left++}for left < right && nums[right] == nums[right-1] {right--}left++right--} else if sum < 0 {left++} else {right--}}}return res
}
3. 反转字符串

问题:原地反转字符串(LeetCode 344)。 思路:左指针从 0,右指针从末尾,交换值后向中间移动。

go

func reverseString(s []byte) {left, right := 0, len(s)-1for left < right {s[left], s[right] = s[right], s[left]left++right--}
}
4. 验证回文串

问题:判断字符串是否为回文(只考虑字母和数字,忽略大小写,LeetCode 125)。 思路:双指针跳过非字母 / 数字,比较对应位置是否相等。

go

import "unicode"
​
func isPalindrome(s string) bool {left, right := 0, len(s)-1for left < right {// 左指针找下一个字母/数字for left < right && !unicode.IsLetterOrNumber(rune(s[left])) {left++}// 右指针找下一个字母/数字for left < right && !unicode.IsLetterOrNumber(rune(s[right])) {right--}// 忽略大小写比较if unicode.ToLower(rune(s[left])) != unicode.ToLower(rune(s[right])) {return false}left++right--}return true
}
5. 移除元素

问题:原地移除值为 val 的元素,返回新长度(LeetCode 27)。 思路:快指针遍历数组,慢指针记录有效元素位置,不等则赋值给慢指针。

go

func removeElement(nums []int, val int) int {slow := 0for fast := 0; fast < len(nums); fast++ {if nums[fast] != val {nums[slow] = nums[fast]slow++}}return slow
}
6. 合并两个有序数组

问题:将 nums2 合并到 nums1 中(假设 nums1 有足够空间,LeetCode 88)。 思路:从后往前双指针,避免覆盖 nums1 中的元素。

go

func merge(nums1 []int, m int, nums2 []int, n int) {i, j, k := m-1, n-1, m+n-1 // i: nums1有效尾,j: nums2尾,k: 合并后尾for i >= 0 && j >= 0 {if nums1[i] > nums2[j] {nums1[k] = nums1[i]i--} else {nums1[k] = nums2[j]j--}k--}// 若nums2还有剩余元素,直接复制for j >= 0 {nums1[k] = nums2[j]j--k--}
}
7. 环形链表(判断环)

问题:判断链表是否有环(LeetCode 141)。 思路:快慢指针,快指针每次走 2 步,慢指针走 1 步,若相遇则有环。

go

type ListNode struct {Val intNext *ListNode
}
​
func hasCycle(head *ListNode) bool {if head == nil || head.Next == nil {return false}slow, fast := head, head.Nextfor slow != fast {if fast == nil || fast.Next == nil {return false // 快指针到尾,无环}slow = slow.Nextfast = fast.Next.Next}return true
}
8. 盛最多水的容器

问题:找到两条线,与 x 轴组成的容器能装最多水(LeetCode 11)。 思路:左右指针从两端出发,面积由短边决定,移动短边指针尝试更大面积。

go

func maxArea(height []int) int {maxArea := 0left, right := 0, len(height)-1for left < right {// 计算当前面积(宽*高)width := right - leftcurrentHeight := min(height[left], height[right])area := width * currentHeightif area > maxArea {maxArea = area}// 移动短边指针if height[left] < height[right] {left++} else {right--}}return maxArea
}func min(a, b int) int {if a < b {return a}return b
}
9. 链表的中间节点

问题:返回链表的中间节点(LeetCode 876)。 思路:快慢指针,快指针到尾时,慢指针在中间。

go

func middleNode(head *ListNode) *ListNode {slow, fast := head, headfor fast != nil && fast.Next != nil {slow = slow.Nextfast = fast.Next.Next}return slow
}
10. 移除重复元素

问题:原地移除有序数组中的重复元素,返回新长度(LeetCode 26)。 思路:慢指针记录有效位置,快指针遍历,不等则赋值给慢指针。

go

func removeDuplicates(nums []int) int {if len(nums) == 0 {return 0}slow := 1 // 第一个元素必保留for fast := 1; fast < len(nums); fast++ {if nums[fast] != nums[fast-1] {nums[slow] = nums[fast]slow++}}return slow
}
11. 分割链表

问题:将链表分割成所有小于 x 的节点在前,大于等于 x 的在后(LeetCode 86)。 思路:用两个链表分别存储小于 x 和大于等于 x 的节点,最后拼接。

go

func partition(head *ListNode, x int) *ListNode {dummy1, dummy2 := &ListNode{}, &ListNode{} // 虚拟头节点p1, p2 := dummy1, dummy2 // 双指针分别指向两个链表for head != nil {if head.Val < x {p1.Next = headp1 = p1.Next} else {p2.Next = headp2 = p2.Next}head = head.Next}p2.Next = nil // 避免环p1.Next = dummy2.Next // 拼接两个链表return dummy1.Next
}
12. 最长回文子串

问题:找到字符串中最长的回文子串(LeetCode 5)。 思路:对每个字符(或两个字符)向两边扩展,用双指针找最长回文。

go

func longestPalindrome(s string) string {start, end := 0, 0for i := 0; i < len(s); i++ {// 奇数长度回文(中心为i)l1, r1 := expandAroundCenter(s, i, i)// 偶数长度回文(中心为i和i+1)l2, r2 := expandAroundCenter(s, i, i+1)// 更新最长回文边界if r1-l1 > end-start {start, end = l1, r1}if r2-l2 > end-start {start, end = l2, r2}}return s[start : end+1]
}
​
// 从l和r向两边扩展,返回最长回文的左右边界
func expandAroundCenter(s string, l, r int) (int, int) {for l >= 0 && r < len(s) && s[l] == s[r] {l--r++}return l + 1, r - 1 // 退出时多移了一步,回退
}

三、LeetCode 双指针题型总结(12 类)
题型分类核心思路典型题目(LeetCode)
两数 / 三数 / 四数之和排序后用双指针减少一层循环,注意去重167(两数)、15(三数)、18(四数)
字符串反转 / 回文相向指针交换或验证对称位置344(反转)、125(回文串)、5(最长回文子串)
数组元素操作(移除 / 去重)同向指针,快指针遍历,慢指针记录有效元素27(移除元素)、26(去重)、80(去重 II)
合并有序数组 / 链表从后往前(数组)或从头往后(链表)双指针,避免覆盖88(合并数组)、21(合并链表)
链表环问题快慢指针,快指针速度是慢指针 2 倍,相遇则有环141(判断环)、142(找环入口)
链表中点 / 倒数第 k 节点快慢指针(中点:快 2 慢 1;倒数 k:快先走 k 步)876(中点)、19(倒数第 k)
容器盛水问题相向指针,由短边决定面积,移动短边寻找更大面积11(盛最多水)
分割链表 / 数组双指针分别记录两类元素,最后拼接86(分割链表)、75(颜色分类)
子数组 / 子串问题滑动窗口(双指针的变种),用左右指针维护窗口范围3(无重复字符子串)、76(最小覆盖子串)
链表相交问题双指针分别遍历两链表,到达尾部后切换到另一链表,相遇点为交点160(相交链表)
有效三角形的个数排序后固定最大边,双指针找两小边满足和大于最大边611
区间列表的交集双指针遍历两个区间列表,取重叠部分,移动较短区间的指针986
四、双指针算法的核心优势
  • 时间优化:将 O (n²) 降至 O (n) 或 O (n log n)(需排序时)。

  • 空间优化:多数情况可原地操作,空间复杂度 O (1)。

  • 适用广泛:数组、链表、字符串等线性结构均适用,尤其适合有序数据。

掌握双指针的关键是:明确指针移动的条件(何时移左、何时移右),以及如何通过指针协作减少冗余计算

双指针算法的本质是通过两个指针的协同移动,利用线性数据结构(数组、链表、字符串)的 “有序性” 或 “连续性”,减少冗余遍历,将高复杂度问题转化为线性复杂度。其核心思想并非简单的 “两个指针”,而是通过指针的 “策略性移动” 来缩小问题规模,本质是对 “搜索空间” 的高效裁剪。

一、双指针的底层逻辑:搜索空间的裁剪

所有双指针问题的核心,都是通过指针移动排除无效的搜索空间,从而降低时间复杂度。

以 “两数之和(有序数组)” 为例:

  • 暴力解法的搜索空间是所有可能的二元组 (i,j)i<j),共 O (n²) 种组合。

  • 双指针解法中,左指针 i 从左向右,右指针 j 从右向左。当 nums[i]+nums[j] < target 时,所有以 i 为左、j' < j 为右的组合都无效(因为数组有序,nums[j'] ≤ nums[j],和会更小),因此直接移动 i 即可;反之则移动 j。每次移动都排除了一整个子集的无效组合,最终仅需 O (n) 次遍历。

关键结论:双指针的有效性依赖于 “移动指针能明确排除部分搜索空间”,这种排除必须是 “绝对的”—— 即被排除的空间中不可能存在解。

二、双指针的分类与策略本质

根据指针的移动方式和目标,双指针可分为三大类,每类的策略本质不同:

1. 相向双指针(左右指针)
  • 形式:指针 left 从起点(0)出发,right 从终点(n-1)出发,向中间移动,直到相遇。

  • 核心场景:利用 “对称性” 或 “区间两端的关联性” 求解(如回文、最值区间)。

  • 移动逻辑:根据两端元素的关系决定移动哪一端,每次移动排除一端的无效选项。

典型案例

  • 回文串判断:对称位置必须相等,否则非回文。

  • 盛最多水的容器:面积由短边决定,移动短边才可能找到更大面积(移动长边只会让面积更小)。

  • 反转字符串:直接交换对称位置元素。

本质:利用线性结构的 “两端对称性”,通过比较两端元素快速缩小范围。

2. 同向双指针(快慢指针)
  • 形式:指针 slowfast 从同一起点出发,fast 移动速度 ≥ slow(通常 fast 走 1 步,slow 走 0 或 1 步)。

  • 核心场景:在序列中 “筛选” 或 “定位” 特定元素(如去重、找中点、移除元素)。

  • 移动逻辑fast 负责 “探索”(遍历所有元素),slow 负责 “记录有效位置”,仅当 fast 满足条件时,slow 才移动并更新值。

典型案例

  • 移除元素:fast 遍历所有元素,slow 只记录非目标值的位置。

  • 链表中点:fast 速度是 slow 的 2 倍,fast 到尾时 slow 在中点。

  • 去重:fast 跳过重复元素,slow 记录不重复的元素。

本质:用 fast 遍历,slow 维护 “有效序列” 的边界,实现 “原地修改” 以节省空间。

3. 滑动窗口(双指针的高级变体)
  • 形式leftright 同向移动,共同维护一个 “区间 [left, right]”,通过调整区间范围求解(如子串、子数组问题)。

  • 核心场景:寻找满足特定条件的 “连续区间”(如最长 / 最短子串、和为 k 的子数组)。

  • 移动逻辑right 先扩张窗口,当窗口不满足条件时,left 收缩窗口,直到重新满足条件。

典型案例

  • 无重复字符的最长子串(LeetCode 3):right 扩张,出现重复时 left 跳到重复位置后。

  • 最小覆盖子串(LeetCode 76):right 扩张到覆盖所有目标字符,left 收缩到最小有效区间。

  • 长度最小的子数组(LeetCode 209):right 扩张到和≥target,left 收缩到最小长度。

本质:通过窗口的 “扩张 - 收缩” 动态维护有效区间,避免对每个区间的重复检查。

三、双指针的适用条件与局限性

适用条件:
  1. 线性结构:仅适用于数组、链表、字符串等线性数据结构(非线性结构如树、图无法直接应用)。

  2. 有序性依赖:多数问题需要数据有序(如两数之和、三数之和),无序时需先排序(增加 O (n log n) 时间)。

  3. 单调性条件:问题需满足 “指针移动方向唯一”—— 即移动后不会出现 “需要回退指针” 的情况(否则双指针失效,需用其他算法)。

局限性:
  1. 对无序数据不友好:若数据无序且无法排序(如链表无法高效排序),双指针可能不如哈希表直接。

  2. 复杂条件难以处理:当判断条件是非线性的(如涉及乘积、模运算),指针移动规则可能不明确,难以排除无效空间。

  3. 无法处理嵌套结构:如二维数组的多数问题(需用二分或 DFS),双指针难以覆盖所有维度。

四、双指针与其他算法的关联

  1. 与排序的协同: 双指针常与排序结合(如三数之和、有效三角形个数),排序的作用是 “赋予数据单调性”,让指针移动规则可明确(如 “小了右移,大了左移”)。

  2. 与哈希表的对比

    • 哈希表优势:无需排序,适合无序数据(如两数之和的无序版本)。

    • 双指针优势:空间复杂度 O (1)(哈希表为 O (n)),适合空间敏感场景。

  3. 与二分查找的异同

    • 相同点:均通过缩小搜索空间降低复杂度。

    • 不同点:二分查找依赖 “全局有序”,每次排除一半空间;双指针依赖 “局部有序性”,每次排除部分空间(比例不固定)。

五、如何判断何时使用双指针?

当遇到以下特征的问题时,优先考虑双指针:

  1. 问题涉及 “两个元素的关系”(如和、差、大小比较)。

  2. 需要 “原地修改” 数组 / 链表(如去重、移除元素)。

  3. 寻找 “连续区间”(如子串、子数组)。

  4. 数据是有序的,或可通过排序变为有序。

总结

双指针算法的核心是 “策略性移动指针以裁剪搜索空间”,其高效性来源于对线性结构特性的充分利用。从简单的反转字符串到复杂的滑动窗口,本质都是通过指针协作减少冗余计算。掌握双指针的关键不在于记住题型,而在于理解 “如何通过指针移动排除无效空间”—— 这需要结合问题的具体条件,设计出明确、无歧义的指针移动规则。

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

相关文章:

  • CCF-GESP 等级考试 2025年6月认证C++七级真题解析
  • PyQt5图形和特效(Qss的UI美化)
  • zabbix-agent静默安装
  • MinIO 用户管理与权限控制详解
  • LINUX727 磁盘管理回顾1;配置文件回顾
  • 数据类型处理流讲解
  • 《中国棒球》cba外援规则·棒球1号位
  • Java排序中(a).compareTo(b)与Integer.compare(a, b)区别
  • Java学习-------外观模式
  • incus套件在 主力 Linux Distros 上的安装配置与基本使用
  • 【NLP实践】三、LLM搭建中文知识库:提供RestfulAPI服务
  • LeetCode第349题_两个数组的交集
  • python 阿里云 安装 dashscope的简介、安装
  • c语言结构体字节对齐
  • github上传本地项目过程记录
  • Spring经典“送命题”:BeanFactory vs FactoryBean
  • Flutter中实现页面跳转功能
  • vulhub-red靶机攻略
  • 深度学习计算(深度学习-李沐-学习笔记)
  • IKAnalyzer分词插件使用方法
  • 第十八章:AI的“通感”:揭秘图、文、音的共同语言——CLIP模型
  • 图像智能识别(一)-Python方向
  • 嵌入式学习日志————对射式红外传感器计次
  • 「iOS」————ARC
  • MyBatis-Plus 条件构造器(Wrapper)全解析
  • docker in docker - 在docker容器中使用宿主机的docker
  • mac电脑安装docker图文教程
  • Java面试全栈通关:从微服务到AI的技术深度解析
  • [10月考试] C
  • Java面试全攻略:Spring生态与微服务架构实战