深入理解【双指针】:从基础概念到实际例题
双指针篇
在 Java 中,双指针(Two Pointers)是一种常见的算法技巧,通常用于解决数组、链表或字符串相关的问题。它通过使用两个指针(索引或引用)来遍历数据结构,以减少时间复杂度或空间复杂度。根据问题的类型和指针的移动方式,双指针可以分为以下几种分类。
双指针类型讲解
1. 对撞指针(Two Pointers Moving Towards Each Other)
- 描述: 两个指针分别从数组的两端向中间移动,直到满足某个条件或相遇。
- 适用场景: 常用于有序数组的查找问题(如两数之和)、反转问题或分区问题。
- 时间复杂度: 通常为 O(n)。
- 例子: 两数之和(Two Sum,假设数组已排序)。
示例代码
public class TwoSum {
public static int[] twoSum(int[] nums, int target) {
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) {
left++;
} else {
right--;
}
}
return new int[]{-1, -1}; // 未找到
}
public static void main(String[] args) {
int[] nums = {2, 7, 11, 15};
int target = 9;
int[] result = twoSum(nums, target);
System.out.println("[" + result[0] + ", " + result[1] + "]"); // 输出 [0, 1]
}
}
2. 快慢指针(Fast and Slow Pointers)
- 描述: 一个指针(快指针)移动速度快,另一个指针(慢指针)移动速度慢,通常快指针每次走两步,慢指针每次走一步。
- 适用场景: 常用于链表问题(如检测环、找中间节点)或数组中的循环检测。
- 时间复杂度: 通常为 O(n)。
- 例子: 检测链表中是否有环(LeetCode 141)。
示例代码
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
public class LinkedListCycle {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
if (slow == fast) { // 相遇说明有环
return true;
}
}
return false;
}
}
3. 同向指针(Two Pointers Moving in the Same Direction)
- 描述: 两个指针从同一端开始,向同一方向移动,通常一个指针用于遍历,另一个指针用于标记或记录。
- 适用场景: 常用于滑动窗口问题、子数组问题或字符串匹配。
- 时间复杂度: 通常为 O(n)。
- 例子: 找到最小的子数组和(LeetCode 209)。
示例代码
public class MinSubArrayLen {
public static int minSubArrayLen(int target, int[] nums) {
int left = 0;
int sum = 0;
int minLen = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
public static void main(String[] args) {
int[] nums = {2, 3, 1, 2, 4, 3};
int target = 7;
System.out.println(minSubArrayLen(target, nums)); // 输出 2
}
}
4. 固定距离指针(Fixed Distance Pointers)
- 描述: 两个指针之间的距离固定,通过移动这两个指针来解决问题。
- 适用场景: 常用于字符串匹配、定长子数组问题。
- 时间复杂度: 通常为 O(n)。
- 例子: 检查字符串中是否有重复字符的最长子串(固定窗口大小)。
示例代码
public class CheckDuplicates {
public static boolean checkDuplicates(String s, int k) {
int[] count = new int[26];
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
count[c - 'a']++;
if (i >= k) {
count[s.charAt(i - k) - 'a']--;
}
if (count[c - 'a'] > 1) {
return true; // 发现重复字符
}
}
return false;
}
public static void main(String[] args) {
String s = "abcba";
int k = 3;
System.out.println(checkDuplicates(s, k)); // 输出 true
}
}
总结
类型 | 指针移动方式 | 典型问题 |
---|---|---|
对撞指针 | 从两端向中间移动 | 两数之和、反转数组 |
快慢指针 | 一个快一个慢 | 链表环检测、中间节点 |
同向指针 | 同方向不同速 | 滑动窗口、子数组问题 |
固定距离指针 | 保持固定间距移动 | 定长子串、重复字符检测 |
双指针的核心在于通过巧妙地移动指针来避免嵌套循环,从而优化算法效率。
二、例题演示
1. 美丽的区间
题目描述
给定一个长度为 nn 的序列 a1,a2,⋯,ana1,a2,⋯,a**n 和一个常数 SS。
对于一个连续区间如果它的区间和大于或等于 SS,则称它为美丽的区间。
对于一个美丽的区间,如果其区间长度越短,它就越美丽。
请你从序列中找出最美丽的区间。
输入描述
第一行包含两个整数 n,Sn,S,其含义如题所述。
接下来一行包含 nn 个整数,分别表示 a1,a2,⋯,ana1,a2,⋯,a**n。
10≤N≤10510≤N≤105,1×ai≤1041×a**i≤104,1≤S≤1081≤S≤108。
输出描述
输出共一行,包含一个整数,表示最美丽的区间的长度。
若不存在任何美丽的区间,则输出 00。
输入输出样例
示例 1
输入
5 6
1 2 3 4 5
输出
2
解题思路:
-
本题双指针的类型属于快慢指针,通过low、high两个指针,实现检索最小数对,通过两个while循环,减少了算法复杂度。
-
本题不可以用排序,排序会导致数组顺序变化,因为前后数字关系他们的和。笔者也是一开始混进了排序的思路漩涡,倒是卡住了。
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class 蓝桥杯真题_美丽的区间 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int s = scan.nextInt();
int[] a = new int[n];
for (int i=0;i<n;i++) {
a[i] = scan.nextInt();
}
int low = 0;
int high = 0;
int res = n;
int sum = 0;
while (high < n) {
sum = sum + a[high];
while(sum>=s) {
res = Math.min(res,high-low+1);
sum = sum -a[low];
low++;
}
high++;
}
if(res == n) {
System.out.println(0);
}
else {
System.out.println(res);
}
scan.close();
}
}
2.聪明的小羊肖恩
问题描述
小羊肖恩是一只非常聪明的绵羊,在牧场里与其他绵羊一起生活。有一天,它在草地上漫步时,发现了一些数字。它猜想这些数字可能在某些方面有用,于是把它们带回了牧场,并开始研究它们。
具体来说,小羊有一个长度为 nn 的数组,第 ii 个数字的值为 aia**i。小羊肖恩心中想了两个数 LL 和 RR,它想知道有多少对下标对 (i,j)(i,j) 满足以下条件:
- 1≤i<j≤n1≤i<j≤n;
- L≤ai+aj≤RL≤a**i+a**j≤R;
请你帮它找出满足条件的下标对数量。
输入格式
第一行输入三个整数 nn,LL 和 RR。
第二行输入 nn 个整数 a1,a2,a3,…,ana1,a2,a3,…,a**n,表示数组 aa。
数据范围保证:1≤n≤2×1051≤n≤2×105,1≤ai≤1091≤a**i≤109,1≤L≤R≤1091≤L≤R≤109。
输出格式
输出一个整数,表示满足条件的下标对数量。
样例输入
3 2 4
1 2 3
样例输出
2
说明
样例中满足条件的下标对有 (1,2)(1,2) 和 (1,3)(1,3)。
思路解答
- 本题类型即是相撞指针类型,通过找到两个点的区间,求出答案。
import java.util.Scanner;
import java.util.Arrays;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class 蓝桥杯真题_聪明的小羊肖恩 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
long n = scan.nextLong();
long l = scan.nextLong();
long r = scan.nextLong();
long res = 0;
long[] a = new long[(int)n];
for (long i=0;i<n;i++) {
a[(int)i] = scan.nextInt();
}
Arrays.sort(a);//先排序,完成数组的单调性
for(long i=0;i<n;i++) {
long j = n-1; //j做右指针
while(j>i&&a[(int)j]+a[(int)i]>r) {
j--;//先找一边
}
long k = i+1;
while (k<=j&&a[(int)i]+a[(int)k]<l) {
k=k+1;
}
res = res +Math.max(0,j-k+1);
}
System.out.print(res);
scan.close();
}
}
3.连续子序列的个数
问题描述
给你一个长度为 nn 的数组 aa 和一个数字 mm ,请你计算这个数组有多少个连续子序列的和大于等于 mm ?
如果两个连续子序列来自数组中的不同位置,我们认为它们是不同的,即使它们在内容上是相同的。
输入格式
第一行输入两个整数表示 nn 和 mm 。
第二行输出 nn 个整数表示 aa 数组的元素。
输出格式
输出一个整数表示 aa 数组有多少个连续子序列的和大于等于 mm 。
样例输入
4 10
6 1 2 7
样例输出
2
思路解答
- 本题类型即是滑动窗口类型,通过找到两个点的区间,求出答案。
import java.util.Scanner;
public class 蓝桥杯真题_连续子序列的个数 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取 n 和 m 的值
long n = scanner.nextLong();
long m = scanner.nextLong();
// 创建长度为 n 的数组来存储输入的元素
long[] a = new long[(int) n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextLong();
}
// 初始化左右指针、当前子数组的和以及满足条件的子数组数量
long left = 0;
long right = 0;
long sum = 0;
long cnt = 0;
// 使用滑动窗口来遍历数组
while (right < n) {
// 累加当前元素到总和
sum += a[(int) right];
// 当总和大于等于 m 时,更新满足条件的子数组数量并调整左指针
while (sum >= m) {
cnt += (n - right);
sum -= a[(int) left];
left++;
}
// 右指针右移
right++;
}
// 输出满足条件的子数组数量
System.out.println(cnt);
scanner.close();
}
}
双指针的核心在于通过巧妙地移动指针来避免嵌套循环,从而优化算法效率。希望这些分类和例子能帮到你!😁