双指针法:高效算法解题的利器

双指针法概述

双指针法是一种通过维护两个指针(索引)来遍历或操作数据结构的算法技巧。通常用于数组、链表等线性结构中,通过指针的移动实现高效操作。

适用场景

  1. 有序数组/链表问题

    例如两数之和、合并有序数组等问题中,双指针可减少时间复杂度。

  2. 滑动窗口

    通过快慢指针维护窗口边界,解决子数组/子字符串相关问题。

  3. 链表操作

    如判断环形链表、寻找链表中点等。

常见类型

同向指针

两个指针从同一侧出发,移动方向相同,但速度不同(如快慢指针)。

对向指针

两个指针分别从首尾出发,向中间移动(如二分查找变种)。

代码示例

有序数组两数之和

java 复制代码
public int[] twoSum(int[] nums, int target) {
    int left = 0, 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) left++;
        else right--;
    }
    return new int[]{-1, -1};
}

快慢指针判环

java 复制代码
public boolean hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

复杂度分析

  • 时间复杂度

    通常为O(n),优于暴力解法的O(n²)。

  • 空间复杂度

    仅需常数空间存储指针,通常为O(1)。

优势与局限性
  • 优势
    减少嵌套循环,降低时间复杂度;空间复杂度通常为常数级。
  • 局限性
    需数据结构满足特定条件(如有序性),否则可能无法直接应用。

典型问题解析

数组类问题

三数之和的双指针解法

给定一个整数数组,找到所有不重复的三元组,使得三元组的和为零。双指针法通常结合排序使用。

  1. 将数组排序,便于跳过重复元素和控制指针移动方向。
  2. 固定第一个数 nums[i],将问题转化为在剩余数组中寻找两数之和等于 -nums[i]
  3. 使用左右指针 leftright 分别指向 i+1 和数组末尾。
  4. 根据当前三数之和与零的关系移动指针:
    • 和小于零,移动 left 向右增大值。
    • 和大于零,移动 right 向左减小值。
    • 和等于零,记录结果并跳过重复值。

数学推导

假设 nums[i] + nums[left] + nums[right] = 0,当 nums[i] 固定时,nums[left] + nums[right] 必须为 -nums[i]。排序后数组的单调性保证了指针移动方向的正确性。

盛最多水的容器

给定一个高度数组,找到两条线形成的容器能盛最多水。双指针从两端向中间移动。

  1. 初始化 leftright 分别指向数组首尾。
  2. 计算当前面积 min(height[left], height[right]) * (right - left)
  3. 移动高度较小的一侧指针,因为移动较高的一侧不会增加面积。

图示说明

height[left] < height[right],移动 left 可能遇到更高的线,从而抵消宽度减少的影响;反之亦然。

字符串类问题

反转字符串的双指针应用

反转字符数组可通过双指针从两端向中间交换元素实现。

  1. 初始化 left = 0right = n-1
  2. 交换 s[left]s[right],随后 left++right--
  3. 终止条件为 left >= right

最长回文子串的中心扩展法

双指针用于从中心向两端扩展判断回文。

  1. 遍历每个字符和字符间隙作为中心。
  2. 初始化左右指针向两侧扩展,直到字符不相等。
  3. 记录最大长度和对应的子串。

时间复杂度

暴力解法为 O(n³),双指针优化至 O(n²)。

链表类问题

环形链表检测的快慢指针

快指针每次移动两步,慢指针移动一步,若相遇则存在环。

终止条件

  • 快指针到达 nullnextnull,说明无环。
  • 快慢指针相遇,说明有环。

相交链表判断的双指针

遍历两个链表,到达末尾时切换到另一链表头部,最终会在相交点相遇。

数学推导

设链表 A 长度为 a + c,链表 B 为 b + cc 为公共部分)。双指针路径均为 a + b + c 时相遇。

双指针法的优化技巧

  1. 动态调整指针移动条件

    根据问题特性定制指针移动策略,如三数之和中跳过重复值。

  2. 预处理简化逻辑

    排序是常见预处理手段,如三数之和、最接近的三数之和等问题。

  3. 避免常见错误

    • 检查指针越界,如 left < right
    • 避免死循环,确保指针每次移动后区间缩小。

双指针法的扩展与进阶应用

双指针法是一种高效的算法技巧,通过与分治、动态规划等算法结合,或在树、图等数据结构中变种应用,可以解决更复杂的问题。

双指针与分治结合案例

合并K个排序链表(LeetCode 23):

  • 使用分治法将链表两两合并,每次合并采用双指针法。
  • 时间复杂度优化为O(N log K),其中N为总节点数,K为链表数量。
java 复制代码
public ListNode mergeKLists(ListNode[] lists) {
    if (lists.length == 0) return null;
    return merge(lists, 0, lists.length - 1);
}

private ListNode merge(ListNode[] lists, int left, int right) {
    if (left == right) return lists[left];
    int mid = left + (right - left) / 2;
    ListNode l1 = merge(lists, left, mid);
    ListNode l2 = merge(lists, mid + 1, right);
    return mergeTwoLists(l1, l2);
}

private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode cur = dummy;
    while (l1 != null && l2 != null) {
        if (l1.val < l2.val) {
            cur.next = l1;
            l1 = l1.next;
        } else {
            cur.next = l2;
            l2 = l2.next;
        }
        cur = cur.next;
    }
    cur.next = l1 != null ? l1 : l2;
    return dummy.next;
}
双指针与动态规划结合案例

最长回文子串(LeetCode 5):

  • 中心扩展法结合动态规划思想,通过双指针向两侧扩展。
  • 时间复杂度O(N²),空间复杂度O(1)。
java 复制代码
public String longestPalindrome(String s) {
    if (s == null || s.length() < 1) return "";
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i);
        int len2 = expandAroundCenter(s, i, i + 1);
        int len = Math.max(len1, len2);
        if (len > end - start) {
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
    while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
        left--;
        right++;
    }
    return right - left - 1;
}
树结构中的双指针变种

二叉搜索树中的两数之和(LeetCode 653):

  • 中序遍历+双指针法,类似有序数组的两数之和。
  • 时间复杂度O(N),空间复杂度O(N)。
java 复制代码
public boolean findTarget(TreeNode root, int k) {
    List<Integer> list = new ArrayList<>();
    inorder(root, list);
    int left = 0, right = list.size() - 1;
    while (left < right) {
        int sum = list.get(left) + list.get(right);
        if (sum == k) return true;
        if (sum < k) left++;
        else right--;
    }
    return false;
}

private void inorder(TreeNode root, List<Integer> list) {
    if (root == null) return;
    inorder(root.left, list);
    list.add(root.val);
    inorder(root.right, list);
}
图结构中的双指针变种

接雨水(LeetCode 42):

  • 将柱状图视为特殊拓扑结构,双指针从两侧向中间移动。
  • 时间复杂度O(N),空间复杂度O(1)。
java 复制代码
public int trap(int[] height) {
    int left = 0, right = height.length - 1;
    int leftMax = 0, rightMax = 0;
    int res = 0;
    while (left < right) {
        if (height[left] < height[right]) {
            if (height[left] >= leftMax) leftMax = height[left];
            else res += leftMax - height[left];
            left++;
        } else {
            if (height[right] >= rightMax) rightMax = height[right];
            else res += rightMax - height[right];
            right--;
        }
    }
    return res;
}

实际工程应用场景:

  • 版本号比较(如"1.01"和"1.001")
  • 大文件差异比对(如git diff)
  • 时间区间合并(如日历应用)
  • 数据库查询优化(如索引区间扫描)

通过掌握这些高阶应用,可以显著提升解决复杂算法问题的能力。建议从基础双指针题目开始,逐步过渡到与其他算法结合的复合题型。

相关推荐
米罗篮2 小时前
DSU并查集 & 拓展欧几里得-逆元
c++·经验分享·笔记·算法·青少年编程
初心未改HD2 小时前
深度学习之MLP与反向传播算法详解
人工智能·深度学习·算法
刀法如飞2 小时前
【Go 字符串查找的 20 种实现方式,用不同思路解决问题】
人工智能·算法·go
技术小黑4 小时前
CNN算法实战系列03 | DenseNet121算法实战与解析
pytorch·深度学习·算法·cnn
wearegogog1234 小时前
三电平SVPWM逆变器仿真指南
单片机·算法
笨笨饿5 小时前
74_SysTick滴答定时器中断
c语言·开发语言·人工智能·单片机·嵌入式硬件·算法·学习方法
pkowner5 小时前
若依分页问题及解决方法
java·前端·算法
呃呃本6 小时前
算法题(栈)
算法