携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情
一、前言
双指针技巧,可分为两类:
- “快、慢指针”: 常见于链表中,是否有环、环的起始位置等。
- “左、右指针”: 常见于数组(字符串)中,比如二分搜索、滑动窗口。
(1)快、慢指针
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) return false;
ListNode fast = head, slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next; // 快指针每次走 2 步
slow = slow.next; // 慢指针每次走 1 步
if (fast == slow) return true; // 最终是否相遇
}
return false;
}
}
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) return null;
ListNode fast = head, slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next; // 快指针每次走 2 步
slow = slow.next; // 慢指针每次走 1 步
if (fast == slow) { // 1. 首次相遇
// 2. 从开始节点开始走,再次相遇则为圆环的开始节点
for (ListNode p = head; p != slow; p = p.next, slow = slow.next);
return slow;
}
}
return null;
}
}
(2)左、右指针
- 二分搜索模板:
int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
}
}
return -1;
}
- 反转数组
void reverse(int [] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
// 交换 nums[left] 和 nums[right]
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
++left;
--right;
}
}
二、题目
(1)旋转字符串(易)
题干分析
这个题目说的是,给你两个字符串 A 和 B,你要判断字符串 A 是否可以通过将左边的若干字符旋转到右边,来得到字符串 B。如果可以就返回 true,否则返回 false。
# 比如说,给你的两个字符串是:
A = "abcde"
B = "cdeab"
# 你可以先将 "abcde" 最左边的 a 旋转到右边,然后再把 b 旋转到右边,就可以把字符串 A 变成字符串 B。因此返回 true。
# 再比如说,给你的两个字符串是:
A = "abcde"
B = "abced"
# 那么 A 就无法通过将左边的若干字符旋转到右边,来得到字符串 B。因此你要返回 false。
思路解法
思路有二: 取巧法 和 双指针法
方法一:取巧法
- 所有变换的结果都是:原字符串 + 原字符串后的子串
// Time: O(n^2), Space: O(1), Faster: 100.00%
public boolean rotateString(String s, String goal) {
return s.length() == goal.length() && (s + s).contains(goal);
}
方法二:双指针法
// 方法二: 双指针法
// Time: O(n^2), Space: O(1)
public boolean rotateStringStrStr(String A, String B) {
if (A.length() != B.length()) return false;
String AA = A + A;
int n = A.length(), nn = 2 * n;
for (int start = 0; start <= n; ++start) {
int i = start, j = 0;
while (i < nn && j < n && AA.charAt(i) == B.charAt(j)) {
++i; ++j;
}
if (j == n) return true;
}
return false;
}
(2)移除数组中指定数字(易)
题干分析
这个题目说的是,给你一个整数数组和一个数字,你要就地(in place) 移除数组中等于给定数字的所有元素。然后返回移除指定数字后的子数组长度。
# 比如说,给你的整数数组是:
[1, 4, 2, 0, 2, 8]
# 你要移除的数字是 2。
# 在这个数组中就地移除数字 2 后,得到:
[1, 4, 0, 8, _, _]
# 因此,你要返回这个子数组的长度 4。
# 原数组的长度是 6,所以子数组 1, 4, 0, 8 后面实际上还有两个位置。但这两个位置上放什么数字都无所谓。因为根据返回的长度 4,就可以确定目标子数组的边界。
思路解法
// Time: O(n), Space: O(1), Faster: 100.00%
public int removeElement(int[] nums, int val) {
if (null == nums || nums.length == 0) return 0;
int slow = 0, faster = 0;
while (faster < nums.length) {
if (nums[faster] != val) {
nums[slow ++] = nums[faster];
}
++faster;
}
return slow;
}
(3)最小覆盖子串(难)
题干分析
这个题目说的是,给你两个字符串 s 和 t,你要在 s 中找到一个最短子串,它包含 t 中所有的字符。如果找不到满足条件的子串,就返回空字符串。
# 比如说,给你的字符串 s 和 t 分别是:
s: adbcacab
t: aab
在字符串 s 中,包含 a,a,b 这三个字符的最短子串是 acab,于是你要返回它。
思路解法
思路:滑动窗口 ,不断去改变窗口的左右边界,在这个过程中记录下满足最短子串的开始下标和长度。
实现需要解决的问题:
- 判断滑动窗口中是否完整已经包含
t
中的全部字符: 存储在 哈希表 中。 - 还需要一个记录
count
: 记录需要的总字符数
// Time: o(n), Space: o(n), Faster: 96.14%
public String minWindow(String s, String t) {
if (s == null || t == null) return "";
// 1. 哈希表:用于记录需要的字符
int [] required = new int[256];
for (int i = 0; i < t.length(); ++i) {
++required[t.charAt(i)];
}
// 2. 记录结果。 count: 记录需要的总字符数
int start = 0, len = Integer.MAX_VALUE, count = t.length();
// 3. 滑动窗口移动
int left = 0, right = 0;
while (right < s.length()) {
char r = s.charAt(right);
if (required[r] > 0) --count; // 命中需要的字符,总字符数 -1
--required[r];
// 判断左侧窗口是否要收缩:已经获得所有字符
while (count == 0) {
// 更新答案
if (right - left + 1 < len) {
start = left;
len = right - left + 1;
}
char l = s.charAt(left);
++required[l];
if (required[l] > 0) ++count;
++left;
}
++right;
}
return len == Integer.MAX_VALUE ? "": s.substring(start, start + len);
}