欢迎光临
我们一直在努力

【超详细】双指针算法 核心知识点全解析(含多场景代码实现 + 生活应用)

一、前言

双指针(Two Pointers)是编程中最经典、最高效的算法技巧之一,其核心是通过两个「指针(索引 / 引用)」遍历数据结构,根据条件动态调整指针位置,从而将嵌套循环的 O(n2) 时间复杂度优化为线性的 O(n)。与二分查找(聚焦「折半缩小范围」)不同,双指针更侧重「线性遍历 + 指针协作」,广泛应用于数组、链表、字符串等线性数据结构的问题求解。本文将从核心概念、分类及原理、多场景代码实现、面试题实战、生活场景应用等维度,帮你彻底掌握双指针的精髓,解决新手最头疼的「指针移动逻辑」「边界处理」等问题。

二、双指针核心概念

2.1 定义

双指针是指在遍历数据结构(数组、链表、字符串等)时,使用两个「指针」(本质是数组索引 / 链表节点引用)指向不同位置,通过指针的移动规则(同向 / 反向、快慢)协作完成任务,核心目标是「减少遍历次数,优化时间复杂度」。

2.2 核心前提

  • 线性数据结构:主要适用于数组、链表、字符串等线性结构(树形 / 图结构不适用);
  • 灵活的指针规则:无固定的「有序」要求(部分场景需有序,如左右指针解两数之和),核心是根据业务逻辑设计指针移动方式;
  • 空间优化:通常仅需额外的常数级空间(O(1)),是「原地算法」的常用实现手段。
  • 2.3 双指针的核心分类(按移动方向)

    类型 特点 典型应用场景
    左右指针(反向指针 两个指针从数组 / 字符串两端向中间移动 有序数组两数之和、字符串反转、回文判断
    快慢指针(龟兔指针) 两个指针从同一端出发,一快一慢移动(如快指针走 2 步,慢指针走 1 步) 链表判环、删除链表倒数第 k 个节点、数组去重
    同向指针(滑动窗口) 两个指针从同一端出发,同方向移动,形成「窗口」范围 最长无重复子串、子数组和为目标值、滑动窗口类问题

    2.4 核心思想

    双指针的本质是「用两个指针替代嵌套循环」,通过一次遍历完成原本需要两层循环的任务:

  • 初始化两个指针的起始位置(如左右指针:left=0,right=n-1;快慢指针:slow=0,fast=0);
  • 根据业务逻辑定义「指针移动规则」(如满足条件则左指针右移,不满足则右指针左移);
  • 遍历过程中通过指针协作完成目标(如查找、修改、统计);
  • 终止条件:指针相遇 / 越界(如左右指针:left >= right;快慢指针:fast == null)。
  • 三、双指针执行流程(可视化)

    3.1 左右指针:有序数组两数之和

    以升序数组 [1,3,5,7,9,11] 找和为 10 的两个数为例:

    初始:left=0(值1),right=5(值11)→ 和=12 > 10 → right左移(right=4,值9) 第1轮:left=0(1)+ right=4(9)=10 → 找到,返回[0,4]

    若找和为8的两个数: 初始:left=0(1)+ right=5(11)=12>8 → right=4(9) 第1轮:1+9=10>8 → right=3(7) 第2轮:1+7=8 → 找到,返回[0,3]

    3.2 快慢指针:数组去重

    以数组 [1,1,2,2,3,4,4,5] 去重为例:

    初始:slow=0(慢指针,记录唯一元素位置),fast=1(快指针,遍历数组) 第1轮:nums[fast]=1 == nums[slow]=1 → fast右移(fast=2) 第2轮:nums[fast]=2 != nums[slow]=1 → slow右移(slow=1),nums[slow]=2,fast右移(fast=3) 第3轮:nums[fast]=2 == nums[slow]=2 → fast右移(fast=4) 第4轮:nums[fast]=3 != nums[slow]=2 → slow右移(slow=2),nums[slow]=3,fast右移(fast=5) … 最终slow=4,去重后数组为[1,2,3,4,5]

    四、多场景代码实现(Java 版)

    4.1 基础版 1:左右指针(有序数组两数之和)

    场景:给定升序数组,找两个数之和等于目标值,返回索引(要求时间 O (n),空间 O (1))。

    /**
    * 左右指针:有序数组两数之和
    */
    public class TwoSumWithTwoPointers {
    public static int[] twoSum(int[] nums, int target) {
    // 1. 边界处理
    if (nums == null || nums.length < 2) {
    return new int[]{-1, -1};
    }

    // 2. 初始化左右指针(两端向中间)
    int left = 0;
    int right = nums.length – 1;

    while (left < right) {
    int sum = nums[left] + nums[right];
    if (sum == target) {
    // 找到目标,返回索引
    return new int[]{left, right};
    } else if (sum > target) {
    // 和太大,右指针左移(减小和)
    right–;
    } else {
    // 和太小,左指针右移(增大和)
    left++;
    }
    }

    // 未找到
    return new int[]{-1, -1};
    }

    // 测试
    public static void main(String[] args) {
    int[] nums = {1,3,5,7,9,11};
    int[] result1 = twoSum(nums, 10);
    System.out.println("和为10的索引:" + result1[0] + "," + result1[1]); // 0,4

    int[] result2 = twoSum(nums, 8);
    System.out.println("和为8的索引:" + result2[0] + "," + result2[1]); // 0,3

    int[] result3 = twoSum(nums, 20);
    System.out.println("和为20的索引:" + result3[0] + "," + result3[1]); // -1,-1
    }
    }

    4.2 基础版 2:快慢指针(数组去重)

    场景:给有序数组原地去重,返回去重后数组长度(要求空间 O (1))。

    /**
    * 快慢指针:有序数组去重(原地修改)
    */
    public class RemoveDuplicates {
    public static int removeDuplicates(int[] nums) {
    // 1. 边界处理
    if (nums == null || nums.length == 0) {
    return 0;
    }

    // 2. 初始化快慢指针(慢指针记录唯一元素位置,快指针遍历)
    int slow = 0; // 慢指针:指向最后一个唯一元素
    for (int fast = 1; fast < nums.length; fast++) {
    // 3. 快指针找到不同元素,慢指针右移并赋值
    if (nums[fast] != nums[slow]) {
    slow++;
    nums[slow] = nums[fast];
    }
    // 相同则仅快指针右移(跳过重复元素)
    }

    // 4. 慢指针+1为去重后数组长度
    return slow + 1;
    }

    // 测试
    public static void main(String[] args) {
    int[] nums = {1,1,2,2,3,4,4,5};
    int newLength = removeDuplicates(nums);
    System.out.println("去重后长度:" + newLength); // 输出5
    System.out.print("去重后数组:");
    for (int i = 0; i < newLength; i++) {
    System.out.print(nums[i] + " "); // 输出1 2 3 4 5
    }
    }
    }

    4.3 基础版 3:快慢指针(链表判环)

    场景:判断单链表是否有环(如尾节点指向中间节点),经典「龟兔赛跑」算法。

    /**
    * 链表节点定义
    */
    class ListNode {
    int val;
    ListNode next;
    ListNode(int val) {
    this.val = val;
    this.next = null;
    }
    }

    /**
    * 快慢指针:判断链表是否有环
    */
    public class LinkedListCycle {
    public static boolean hasCycle(ListNode head) {
    // 1. 边界处理
    if (head == null || head.next == null) {
    return false;
    }

    // 2. 初始化快慢指针(慢指针走1步,快指针走2步)
    ListNode slow = head; // 龟
    ListNode fast = head.next; // 兔

    while (slow != fast) {
    // 快指针走到头,无环
    if (fast == null || fast.next == null) {
    return false;
    }
    slow = slow.next; // 慢指针走1步
    fast = fast.next.next; // 快指针走2步
    }

    // 快慢指针相遇,有环
    return true;
    }

    // 测试
    public static void main(String[] args) {
    // 构造有环链表:1→2→3→2
    ListNode node1 = new ListNode(1);
    ListNode node2 = new ListNode(2);
    ListNode node3 = new ListNode(3);
    node1.next = node2;
    node2.next = node3;
    node3.next = node2; // 环
    System.out.println("链表是否有环:" + hasCycle(node1)); // 输出true

    // 构造无环链表:1→2→3→null
    ListNode node4 = new ListNode(1);
    ListNode node5 = new ListNode(2);
    ListNode node6 = new ListNode(3);
    node4.next = node5;
    node5.next = node6;
    System.out.println("链表是否有环:" + hasCycle(node4)); // 输出false
    }
    }

    4.4 进阶版:滑动窗口(最长无重复子串)

    场景:找字符串中最长无重复字符的子串长度(同向双指针的典型应用)。

    import java.util.HashSet;
    import java.util.Set;

    /**
    * 同向指针(滑动窗口):最长无重复子串
    */
    public class LengthOfLongestSubstring {
    public static int lengthOfLongestSubstring(String s) {
    // 1. 边界处理
    if (s == null || s.length() == 0) {
    return 0;
    }

    // 2. 初始化同向指针(滑动窗口左右边界)+ 存储窗口内字符
    Set<Character> charSet = new HashSet<>();
    int left = 0; // 窗口左边界
    int maxLen = 0; // 最长长度

    // 3. 右指针遍历字符串(窗口右边界)
    for (int right = 0; right < s.length(); right++) {
    char c = s.charAt(right);
    // 4. 有重复字符,左指针右移(缩小窗口)直到无重复
    while (charSet.contains(c)) {
    charSet.remove(s.charAt(left));
    left++;
    }
    // 5. 添加当前字符到窗口
    charSet.add(c);
    // 6. 更新最长长度
    maxLen = Math.max(maxLen, right – left + 1);
    }

    return maxLen;
    }

    // 测试
    public static void main(String[] args) {
    System.out.println(lengthOfLongestSubstring("abcabcbb")); // 输出3("abc")
    System.out.println(lengthOfLongestSubstring("bbbbb")); // 输出1("b")
    System.out.println(lengthOfLongestSubstring("pwwkew")); // 输出3("wke")
    }
    }

    五、双指针的优缺点 & 适用场景

    维度 优点 缺点
    时间复杂度 通常为 O(n),相比嵌套循环的 O(n2) 大幅优化 指针移动规则需根据场景定制,逻辑设计有一定门槛
    空间复杂度 多数场景为 O(1)(原地算法),仅用常数级空间 滑动窗口部分场景需额外空间(如哈希表),但仍为 O(n) 级别
    适用范围 覆盖数组、链表、字符串等绝大多数线性结构问题 不适用于树形、图等非线性结构
    实现难度 代码简洁,核心逻辑清晰 边界条件(如指针越界、相遇)易出错,需仔细处理

    适用场景

  • 数组 / 字符串的「查找类」问题(两数之和、三数之和、最长子串);
  • 数组 / 链表的「修改类」问题(去重、反转、删除指定元素);
  • 「遍历优化」问题(将嵌套循环降为线性遍历);
  • 「窗口类」问题(滑动窗口求子数组 / 子串的最值、统计)。
  • 六、经典面试题实战(附思路 + 代码)

    6.1 题目 1:反转字符串(LeetCode 344)

    题目描述:原地反转字符数组(如 ['h','e','l','l','o'] → ['o','l','l','e','h'])。思路:左右指针从两端向中间移动,交换指针指向的字符,直到指针相遇。

    /**
    * 左右指针:反转字符串(原地修改)
    */
    public class ReverseString {
    public static void reverseString(char[] s) {
    if (s == null || s.length <= 1) {
    return;
    }

    int left = 0;
    int right = s.length – 1;
    while (left < right) {
    // 交换左右指针字符
    char temp = s[left];
    s[left] = s[right];
    s[right] = temp;
    // 指针移动
    left++;
    right–;
    }
    }

    public static void main(String[] args) {
    char[] s = {'h','e','l','l','o'};
    reverseString(s);
    System.out.println(s); // 输出olleh
    }
    }

    6.2 题目 2:删除链表的倒数第 N 个节点(LeetCode 19)

    题目描述:删除单链表的倒数第 n 个节点,返回头节点。思路:快慢指针,快指针先走 n 步,然后快慢指针同步走,快指针到尾时,慢指针指向倒数第 n 个节点的前一个节点。

    /**
    * 快慢指针:删除链表倒数第N个节点
    */
    public class RemoveNthFromEnd {
    public static ListNode removeNthFromEnd(ListNode head, int n) {
    // 虚拟头节点(处理删除头节点的情况)
    ListNode dummy = new ListNode(0);
    dummy.next = head;

    // 初始化快慢指针
    ListNode fast = dummy;
    ListNode slow = dummy;

    // 快指针先走n步
    for (int i = 0; i < n; i++) {
    fast = fast.next;
    }

    // 快慢指针同步走,直到快指针到尾
    while (fast.next != null) {
    fast = fast.next;
    slow = slow.next;
    }

    // 删除慢指针的下一个节点(倒数第n个)
    slow.next = slow.next.next;

    return dummy.next;
    }

    // 辅助方法:遍历链表
    public static void traverse(ListNode head) {
    ListNode cur = head;
    while (cur != null) {
    System.out.print(cur.val + " -> ");
    cur = cur.next;
    }
    System.out.println("null");
    }

    public static void main(String[] args) {
    // 构造链表:1→2→3→4→5
    ListNode head = new ListNode(1);
    head.next = new ListNode(2);
    head.next.next = new ListNode(3);
    head.next.next.next = new ListNode(4);
    head.next.next.next.next = new ListNode(5);

    // 删除倒数第2个节点(4)
    ListNode newHead = removeNthFromEnd(head, 2);
    traverse(newHead); // 输出1 -> 2 -> 3 -> 5 -> null
    }
    }

    6.3 题目 3:三数之和(LeetCode 15)

    题目描述:给数组找所有不重复的三元组 [a,b,c],满足 a+b+c=0。思路:排序 + 左右指针,固定第一个数,用左右指针找另外两个数,时间 O (n²)。

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;

    /**
    * 排序+左右指针:三数之和
    */
    public class ThreeSum {
    public static List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    // 1. 边界处理
    if (nums == null || nums.length < 3) {
    return result;
    }

    // 2. 排序(去重+满足左右指针条件)
    Arrays.sort(nums);

    // 3. 固定第一个数,左右指针找另外两个数
    for (int i = 0; i < nums.length; i++) {
    // 去重:第一个数重复则跳过
    if (i > 0 && nums[i] == nums[i-1]) {
    continue;
    }

    int target = -nums[i]; // b + c = -a
    int left = i + 1;
    int right = nums.length – 1;

    while (left < right) {
    int sum = nums[left] + nums[right];
    if (sum == target) {
    // 找到三元组,加入结果
    result.add(Arrays.asList(nums[i], nums[left], nums[right]));

    // 去重:左指针重复
    while (left < right && nums[left] == nums[left+1]) {
    left++;
    }
    // 去重:右指针重复
    while (left < right && nums[right] == nums[right-1]) {
    right–;
    }

    // 指针移动
    left++;
    right–;
    } else if (sum < target) {
    left++;
    } else {
    right–;
    }
    }
    }

    return result;
    }

    public static void main(String[] args) {
    int[] nums = {-1,0,1,2,-1,-4};
    List<List<Integer>> result = threeSum(nums);
    System.out.println("三数之和为0的三元组:");
    for (List<Integer> list : result) {
    System.out.println(list); // 输出[-1,-1,2]、[-1,0,1]
    }
    }
    }

    七、生活中的双指针实战(3 个场景 + 完整代码)

    双指针并非只适用于算法题,生活中很多场景能通过双指针思想提升效率,以下是 3 个典型场景及 Java 实现。

    7.1 场景 1:超市找两件商品总价等于预算

    7.1.1 场景描述

    你有固定预算(如 100 元),超市货架上的商品价格按升序排列(如 [20,30,40,50,60,70]),用双指针快速找两件商品总价刚好等于预算的组合,替代逐个试算。

    7.1.2 解决思路
    • 左右指针分别指向价格数组两端;
    • 计算两数之和:和等于预算则找到;和大于预算则右指针左移;和小于预算则左指针右移;
    • 遍历至指针相遇,未找到则返回无。
    7.1.3 Java 代码实现

    /**
    * 生活场景1:双指针找两件商品总价等于预算
    */
    public class FindTwoGoodsByBudget {
    /**
    * 查找总价等于预算的两件商品价格
    * @param prices 升序的商品价格数组
    * @param budget 预算
    * @return 价格组合,未找到返回空数组
    */
    public static int[] findTwoGoods(int[] prices, int budget) {
    if (prices == null || prices.length < 2) {
    return new int[0];
    }

    int left = 0;
    int right = prices.length – 1;

    while (left < right) {
    int sum = prices[left] + prices[right];
    if (sum == budget) {
    return new int[]{prices[left], prices[right]};
    } else if (sum > budget) {
    // 总价超预算,选更便宜的(右指针左移)
    right–;
    } else {
    // 总价不足,选更贵的(左指针右移)
    left++;
    }
    }

    // 未找到
    return new int[0];
    }

    public static void main(String[] args) {
    int[] prices = {20,30,40,50,60,70};
    int budget1 = 100;
    int[] result1 = findTwoGoods(prices, budget1);
    if (result1.length == 2) {
    System.out.println("预算" + budget1 + "元的组合:" + result1[0] + "+" + result1[1]); // 30+70 或 40+60
    } else {
    System.out.println("预算" + budget1 + "元无匹配组合");
    }

    int budget2 = 150;
    int[] result2 = findTwoGoods(prices, budget2);
    if (result2.length == 2) {
    System.out.println("预算" + budget2 + "元的组合:" + result2[0] + "+" + result2[1]);
    } else {
    System.out.println("预算" + budget2 + "元无匹配组合"); // 输出无
    }
    }
    }

    7.2 场景 2:排队找最长连续无重复人员的队伍

    7.2.1 场景描述

    超市结账队伍中,人员按编号排列(如 [1,2,3,2,4,5,5,6]),用双指针(滑动窗口)找最长连续无重复编号的子队伍,方便收银员快速引导至该队伍。

    7.2.2 解决思路
    • 同向指针(滑动窗口):left 为窗口左边界,right 为窗口右边界;
    • 用集合存储窗口内的人员编号,遇到重复则移动左边界直到无重复;
    • 记录窗口的最大长度,即为最长无重复队伍长度。
    7.2.3 Java 代码实现

    import java.util.HashSet;
    import java.util.Set;

    /**
    * 生活场景2:双指针找最长连续无重复人员的队伍
    */
    public class FindLongestUniqueQueue {
    /**
    * 查找最长连续无重复人员编号的队伍长度
    * @param personIds 人员编号数组
    * @return 最长长度
    */
    public static int findLongestUniqueQueue(int[] personIds) {
    if (personIds == null || personIds.length == 0) {
    return 0;
    }

    Set<Integer> idSet = new HashSet<>();
    int left = 0;
    int maxLen = 0;

    for (int right = 0; right < personIds.length; right++) {
    int curId = personIds[right];
    // 有重复,移动左边界
    while (idSet.contains(curId)) {
    idSet.remove(personIds[left]);
    left++;
    }
    idSet.add(curId);
    // 更新最长长度
    maxLen = Math.max(maxLen, right – left + 1);
    }

    return maxLen;
    }

    public static void main(String[] args) {
    int[] personIds = {1,2,3,2,4,5,5,6};
    int maxLen = findLongestUniqueQueue(personIds);
    System.out.println("最长连续无重复人员的队伍长度:" + maxLen); // 输出4([2,4,5,6])

    int[] personIds2 = {1,1,1,1};
    int maxLen2 = findLongestUniqueQueue(personIds2);
    System.out.println("最长连续无重复人员的队伍长度:" + maxLen2); // 输出1
    }
    }

    7.3 场景 3:快递站找循环堆放的包裹链

    7.3.1 场景描述

    快递站的包裹按「链表」方式堆放(每个包裹贴有下一个包裹的编号),可能出现循环(如包裹 1→2→3→2),用快慢指针快速判断是否有循环,避免分拣时无限遍历。

    7.3.2 解决思路
    • 快慢指针:慢指针每次找下一个包裹,快指针每次找下下个包裹;
    • 若快慢指针指向同一个包裹,说明有循环;若快指针找到末尾(无下一个包裹),则无循环。
    7.3.3 Java 代码实现

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 生活场景3:双指针判断包裹链是否有循环
    */
    public class CheckPackageCycle {
    // 模拟包裹映射:key=当前包裹编号,value=下一个包裹编号(null表示无)
    private static Map<Integer, Integer> packageMap = new HashMap<>();

    /**
    * 判断包裹链是否有循环
    * @param startId 起始包裹编号
    * @return 是否有循环
    */
    public static boolean hasPackageCycle(int startId) {
    // 无起始包裹
    if (!packageMap.containsKey(startId)) {
    return false;
    }

    // 快慢指针(包裹编号)
    int slow = startId;
    int fast = startId;

    while (true) {
    // 慢指针走1步
    slow = packageMap.get(slow);
    // 快指针走1步
    fast = packageMap.get(fast);
    // 快指针无下一个包裹,无循环
    if (fast == null) {
    return false;
    }
    // 快指针再走1步
    fast = packageMap.get(fast);
    // 快指针走到头,无循环
    if (fast == null) {
    return false;
    }
    // 快慢指针相遇,有循环
    if (slow == fast) {
    return true;
    }
    }
    }

    public static void main(String[] args) {
    // 构造有循环的包裹链:1→2→3→2
    packageMap.put(1, 2);
    packageMap.put(2, 3);
    packageMap.put(3, 2);
    System.out.println("包裹链是否有循环:" + hasPackageCycle(1)); // 输出true

    // 清空并构造无循环的包裹链:4→5→6→null
    packageMap.clear();
    packageMap.put(4, 5);
    packageMap.put(5, 6);
    packageMap.put(6, null);
    System.out.println("包裹链是否有循环:" + hasPackageCycle(4)); // 输出false
    }
    }

    八、总结

    核心要点回顾

  • 核心分类:双指针主要分「左右指针(反向)」「快慢指针(同速差)」「同向指针(滑动窗口)」三类,需根据场景选择;
  • 核心优势:将嵌套循环的 O(n2) 优化为线性 O(n),空间复杂度通常为 O(1)(原地算法);
  • 关键逻辑:
    • 左右指针:两端向中间移动,适用于有序结构的查找 / 反转;
    • 快慢指针:同方向一快一慢,适用于链表判环、数组去重;
    • 滑动窗口:同向指针形成窗口,适用于子串 / 子数组的最值统计;
  • 生活应用:只要是「线性遍历 + 条件筛选」的场景,都能通过双指针提升效率(如找商品组合、找无重复队伍、判循环)。
  • 学习建议

  • 先掌握基础的左右 / 快慢指针,再攻克滑动窗口(进阶),重点理解「指针移动规则」;
  • 手动模拟指针移动流程(如在纸上画指针位置变化),解决「边界越界」「重复处理」问题;
  • 尝试将生活中的线性遍历场景转化为双指针问题,加深对思想的理解;
  • 刷 LeetCode 双指针专题(标签:双指针、滑动窗口),从简单到中等难度,巩固实战能力。

  • 文末小结:双指针的核心不是「写代码」,而是「指针协作的思想」—— 用两个指针替代嵌套循环,通过一次遍历完成任务。无论是算法题还是日常生活,只要抓住「线性结构」和「指针移动规则」两个核心,就能用双指针解决问题,大幅提升效率。建议多写、多测、多模拟,让双指针成为你的「算法优化利器」。

    赞(0)
    未经允许不得转载:171主机测评 » 【超详细】双指针算法 核心知识点全解析(含多场景代码实现 + 生活应用)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址