蓝桥杯 Java B 组之双指针技巧(快慢指针、滑动窗口)
Day 5:双指针技巧(快慢指针、滑动窗口)
双指针技巧是处理许多算法问题时常用的技巧,尤其在数组或字符串中。双指针可以帮助我们在遍历过程中减少不必要的运算,从而优化时间复杂度。
📖 一、双指针概述
双指针技巧主要分为两种常见方式:
- 快慢指针(Floyd's Tortoise and Hare):适用于一些涉及到链表、循环、排序等问题。常见于快慢指针查找问题,如链表环问题、判断回文字符串等。
- 滑动窗口:适用于数组或字符串中涉及到子数组、子串问题。通过维护一个可变的窗口范围,避免了频繁的重复计算,从而提高效率。
📖 二、快慢指针(Floyd's Tortoise and Hare)
1. 快慢指针基础
快慢指针技巧最常用于链表问题,主要思想是让两个指针以不同速度进行遍历,快速指针每次移动两步,慢速指针每次移动一步,通常用于判断链表是否有环、链表中环的入口等。
2. 应用示例:判断链表是否有环
问题描述: 给定一个链表,判断该链表是否有环。
- 快指针每次走两步,慢指针每次走一步。
- 如果快指针追上慢指针,则链表有环;否则没有环。
代码实现(判断链表是否有环):
public class LinkedListCycle {
// 链表节点定义
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public boolean hasCycle(ListNode head) {
if (head == 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; // 快指针遍历到末尾,说明没有环
}
public static void main(String[] args) {
LinkedListCycle solution = new LinkedListCycle();
ListNode head = solution.new ListNode(1);
head.next = solution.new ListNode(2);
head.next.next = solution.new ListNode(3);
head.next.next.next = solution.new ListNode(4);
head.next.next.next.next = head.next; // 形成环
boolean result = solution.hasCycle(head);
System.out.println(result); // 输出 true
}
}
3. 代码解析:
- 我们通过定义两个指针,快指针每次移动两步,慢指针每次移动一步。
- 如果链表有环,快指针和慢指针一定会相遇。
- 时间复杂度是 O(n),空间复杂度是 O(1),因为我们只使用了常数空间。
📖 三、滑动窗口
1. 滑动窗口简介
滑动窗口技巧常用于解决字符串或数组中的子串、子数组问题。主要思想是通过维护一个动态窗口,在窗口大小固定或变化的情况下,优化对数据的处理。
滑动窗口有两种类型:
- 固定窗口:窗口大小固定,滑动时,窗口的左右边界会同时变化。
- 可变窗口:窗口大小可变,滑动时,窗口的左边界和右边界根据某些条件变化。
2. 应用示例:移除元素
问题描述: 给定一个数组 nums
和一个值 val
,移除数组中所有等于 val
的元素,并返回移除后的新数组的长度。
思路与分析:
- 维护一个指针
i
,用于遍历数组nums
。 - 用另一个指针
k
来标记不等于val
的元素的位置。 - 遍历完数组后,返回
k
即为新数组的长度。
代码实现(移除元素):
public class RemoveElement {
public int removeElement(int[] nums, int val) {
int k = 0; // 用于标记新的数组长度
for (int i = 0; i < nums.length; i++) {
if (nums[i] != val) { // 如果当前元素不等于val
nums[k] = nums[i]; // 将当前元素放到新数组的当前位置
k++; // 更新新数组的长度
}
}
return k; // 返回新数组的长度
}
public static void main(String[] args) {
RemoveElement solution = new RemoveElement();
int[] nums = {3, 2, 2, 3, 4, 5, 3};
int val = 3;
int newLength = solution.removeElement(nums, val);
System.out.println("新数组长度: " + newLength); // 输出 4
}
}
3. 代码解析:
- 这里使用两个指针:
i
用于遍历原数组,k
用于标记新数组的位置。 - 通过不断检查每个元素是否等于
val
,如果不等于val
,就将其保留在新数组中。 - 时间复杂度是 O(n),空间复杂度是 O(1),因为我们直接在原数组上修改。
4. 应用示例:最小覆盖子串
问题描述: 给定一个字符串 s
和一个字符串 t
,返回 s
中包含 t
所有字符的最小子串。如果 s
中没有包含 t
的子串,返回空字符串。
思路与分析:
- 使用滑动窗口的技巧。
- 定义两个指针:左指针和右指针。右指针扩展窗口,左指针收缩窗口。
- 窗口内的字符包含
t
的所有字符时,记录当前窗口的最小长度,并尝试收缩窗口。
代码实现(最小覆盖子串):
import java.util.HashMap;
import java.util.Map;
public class MinWindowSubstring {
public String minWindow(String s, String t) {
if (s.length() < t.length()) return "";
Map<Character, Integer> tMap = new HashMap<>();
for (char c : t.toCharArray()) {
tMap.put(c, tMap.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0, valid = 0, minLength = Integer.MAX_VALUE, start = 0;
Map<Character, Integer> window = new HashMap<>();
while (right < s.length()) {
char c = s.charAt(right);
right++;
if (tMap.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(tMap.get(c))) {
valid++;
}
}
while (valid == tMap.size()) {
if (right - left < minLength) {
minLength = right - left;
start = left;
}
char d = s.charAt(left);
left++;
if (tMap.containsKey(d)) {
if (window.get(d).equals(tMap.get(d))) {
valid--;
}
window.put(d, window.get(d) - 1);
}
}
}
return minLength == Integer.MAX_VALUE ? "" : s.substring(start, start + minLength);
}
public static void main(String[] args) {
MinWindowSubstring solution = new MinWindowSubstring();
String s = "ADOBECODEBANC";
String t = "ABC";
String result = solution.minWindow(s, t);
System.out.println("最小覆盖子串: " + result); // 输出 "BANC"
}
}
5. 代码解析:
- 我们使用两个指针来维护一个滑动窗口,
right
扩展窗口,left
收缩窗口。 - 使用一个哈希表
tMap
记录目标字符串t
中每个字符的出现次数,使用另一个哈希表window
记录当前窗口中的字符计数。 - valid 记录当前窗口中已包含多少个字符的数量。
时间复杂度:
- O(n),其中
n
是字符串s
的长度。我们只需要遍历一次s
和t
。
📖 四、总结
双指针技巧常见应用:
- 快慢指针:用于链表循环检测、回文判断等。
- 滑动窗口:用于处理字符串/数组中的子串、子数组问题,特别是在要求优化时间复杂度时非常有效。
常见易错点:
- 快慢指针:常常忘记检查快指针是否为空,或者不小心错过了链表是否有环的判断。
- 滑动窗口:窗口范围的扩展和收缩条件要精确,容易遗漏边界条件,比如窗口中的字符是否包含目标字符串的所有字符。