双指针法概述
双指针法是一种通过维护两个指针(索引)来遍历或操作数据结构的算法技巧。通常用于数组、链表等线性结构中,通过指针的移动实现高效操作。
适用场景
-
有序数组/链表问题
例如两数之和、合并有序数组等问题中,双指针可减少时间复杂度。
-
滑动窗口
通过快慢指针维护窗口边界,解决子数组/子字符串相关问题。
-
链表操作
如判断环形链表、寻找链表中点等。
常见类型
同向指针
两个指针从同一侧出发,移动方向相同,但速度不同(如快慢指针)。
对向指针
两个指针分别从首尾出发,向中间移动(如二分查找变种)。
代码示例
有序数组两数之和
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)。
优势与局限性
- 优势 :
减少嵌套循环,降低时间复杂度;空间复杂度通常为常数级。 - 局限性 :
需数据结构满足特定条件(如有序性),否则可能无法直接应用。
典型问题解析
数组类问题
三数之和的双指针解法
给定一个整数数组,找到所有不重复的三元组,使得三元组的和为零。双指针法通常结合排序使用。
- 将数组排序,便于跳过重复元素和控制指针移动方向。
- 固定第一个数
nums[i],将问题转化为在剩余数组中寻找两数之和等于-nums[i]。 - 使用左右指针
left和right分别指向i+1和数组末尾。 - 根据当前三数之和与零的关系移动指针:
- 和小于零,移动
left向右增大值。 - 和大于零,移动
right向左减小值。 - 和等于零,记录结果并跳过重复值。
- 和小于零,移动
数学推导
假设 nums[i] + nums[left] + nums[right] = 0,当 nums[i] 固定时,nums[left] + nums[right] 必须为 -nums[i]。排序后数组的单调性保证了指针移动方向的正确性。
盛最多水的容器
给定一个高度数组,找到两条线形成的容器能盛最多水。双指针从两端向中间移动。
- 初始化
left和right分别指向数组首尾。 - 计算当前面积
min(height[left], height[right]) * (right - left)。 - 移动高度较小的一侧指针,因为移动较高的一侧不会增加面积。
图示说明
若 height[left] < height[right],移动 left 可能遇到更高的线,从而抵消宽度减少的影响;反之亦然。
字符串类问题
反转字符串的双指针应用
反转字符数组可通过双指针从两端向中间交换元素实现。
- 初始化
left = 0和right = n-1。 - 交换
s[left]和s[right],随后left++和right--。 - 终止条件为
left >= right。
最长回文子串的中心扩展法
双指针用于从中心向两端扩展判断回文。
- 遍历每个字符和字符间隙作为中心。
- 初始化左右指针向两侧扩展,直到字符不相等。
- 记录最大长度和对应的子串。
时间复杂度
暴力解法为 O(n³),双指针优化至 O(n²)。
链表类问题
环形链表检测的快慢指针
快指针每次移动两步,慢指针移动一步,若相遇则存在环。
终止条件
- 快指针到达
null或next为null,说明无环。 - 快慢指针相遇,说明有环。
相交链表判断的双指针
遍历两个链表,到达末尾时切换到另一链表头部,最终会在相交点相遇。
数学推导
设链表 A 长度为 a + c,链表 B 为 b + c(c 为公共部分)。双指针路径均为 a + b + c 时相遇。
双指针法的优化技巧
-
动态调整指针移动条件
根据问题特性定制指针移动策略,如三数之和中跳过重复值。
-
预处理简化逻辑
排序是常见预处理手段,如三数之和、最接近的三数之和等问题。
-
避免常见错误
- 检查指针越界,如
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)
- 时间区间合并(如日历应用)
- 数据库查询优化(如索引区间扫描)
通过掌握这些高阶应用,可以显著提升解决复杂算法问题的能力。建议从基础双指针题目开始,逐步过渡到与其他算法结合的复合题型。