双指针技巧是处理许多算法问题时常用的技巧,尤其在数组或字符串中。双指针可以帮助我们在遍历过程中减少不必要的运算,从而优化时间复杂度。
双指针技巧主要分为两种常见方式:
快慢指针技巧最常用于链表问题,主要思想是让两个指针以不同速度进行遍历,快速指针每次移动两步,慢速指针每次移动一步,通常用于判断链表是否有环、链表中环的入口等。
问题描述: 给定一个链表,判断该链表是否有环。
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
}
}
滑动窗口技巧常用于解决字符串或数组中的子串、子数组问题。主要思想是通过维护一个动态窗口,在窗口大小固定或变化的情况下,优化对数据的处理。
滑动窗口有两种类型:
问题描述: 给定一个数组 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
}
}
i
用于遍历原数组,k
用于标记新数组的位置。val
,如果不等于 val
,就将其保留在新数组中。问题描述: 给定一个字符串 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 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 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"
}
}
right
扩展窗口,left
收缩窗口。tMap
记录目标字符串 t
中每个字符的出现次数,使用另一个哈希表 window
记录当前窗口中的字符计数。n
是字符串 s
的长度。我们只需要遍历一次 s
和 t
。