神奇的算法:双指针
1、算法解释
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的 区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。 如:翻转数组、二分搜索。
2、快慢指针
快慢指针是双指针中的一种情况,利用两个指针移动速度的差异进行结题。
快慢指针的前进方向相同,且它们步伐的「差」是恒定的,根据这种确定性去解决链表中的一些问题。使用这种思想还可以解决链表的以下问题:
-
第19题:倒数第k个结点
-
第141题:环形链表
-
第876题:链表的中间结点
…
LeetCode19:倒数第k个结点
题目描述:
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
题解:
- 创建指针fast和slow均指向head,首先要先找到快指针fast的位置,fast与slow之间间隔n-1个节点。
- 同时使用fast和slow对链表进行遍历,直到fast或者fast.next为空。
- 当fast==null时,找了倒数第n+1个节点,此时slow的下一个节点就是要删除的节点。
- 当fast.next==null时,找了倒数第n个节点(也就是头节点),将head向下即可。
代码展示:
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode fast = head, slow = head;
//找出快指针的位置
for(int i = 0; i < n; i++) fast = fast.next;
while(true){
if(fast == null)return head = head.next;
else if(fast.next == null){
slow.next = slow.next.next;
return head;
}
//同时移动两个指针
slow = slow.next;
fast = fast.next;
}
}
LeetCode141:环形链表
题目描述:
给你一个链表的头节点 head
,判断链表中是否有环。
题解:
定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
代码展示:
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null){
return false;
}
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if (fast == slow){
return true;
}
}
return false;
}
LeetCode876:链表的中间结点
题目描述:
给定一个头结点为 head
的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
题解:
定义两个指针,一个快指针每次走两步,一个慢指针每次走一步,当快指针走到链表末尾时,慢指针刚好指向中间结点。
代码展示:
//快慢指针
public ListNode middleNode2(ListNode head) {
// 当q走到末尾时p正好走到中间
ListNode p = head, q = head;
while (q != null && q.next != null) {
q = q.next.next; // 走两步
p = p.next; // 走一步
}
return p;
}
3、方向相反的双指针
在同一个数组中,一个指针指向头部向右移动,一个指向尾部左移动,当两个指针相遇后结束循环。常见有以下例题:
-
第167题:两数之和 II - 输入有序数组
-
第344题:反转字符串
-
第977题:有序数组的平方
…
LeetCode167:两数之和 II - 输入有序数组
题目描述:
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
题解:
因为给定的数组是有序的,我们将一个指针left从左开始(最小值)向右移动,另一个指针right从右开始(最大值)向左移动,当两个指针之和大于target时,则right–,小于target时,则left++,等于target,则返回结果。
func twoSum(numbers []int, target int) []int {
left, right := 0, len(numbers)-1
ret := make([]int, 2)
for left <= right {
if numbers[left]+numbers[right] > target {
right--
} else if numbers[left]+numbers[right] < target {
left++
} else {
ret[0] = left + 1
ret[1] = right + 1
return ret
}
}
return nil
}
LeetCode977:有序数组的平方
题目描述:
给你一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
题解:
数组是有序排列的。因此,数组平方的最大值只能在数组的两端,不是最左边就是最右边,不可能是中间,因为负数平方之后也可能成为最大值。
我们可以使用双指针的方法。left指向数组左端,right指向数组的右端。idx指向结果数组的末尾。
代码展示:
func sortedSquares(nums []int) []int {
ret := make([]int, len(nums))
//idx是指向结果数组ret的下标
left, right,idx := 0, len(nums)-1,len(nums)-1
//ret数组从后往前遍历
for left <= right {
if nums[left]*nums[left] < nums[right]*nums[right] {
ret[idx] = nums[right]*nums[right]
right--
}else {
ret[idx] = nums[left] *nums[left]
left++
}
idx--
}
return ret
}
4、滑动窗口
滑动窗口就是有一个大小可变的窗口, 这个窗口可以是固定长度,也可以是可变长度,左右两端方向一致的向前滑动(右端固定,左端滑动;左端固定,右端滑动)。
滑动窗口算法就是用以解决数组/字符串的子元素问题。可以将嵌套的for循环问题,转换为单循环问题,降低时间复杂度。
LeetCode3:无重复字符的最长子串
题目描述:
给定一个字符串 s
,请你找出其中不含有重复字符的最长子串的长度。
题解:
本题使用滑动窗口求解,[left, right]表示滑动窗口范围,使用Set集合存储不重复的字符。当set集合中不存在当前字符,则将其加入set中,窗口右端移动,窗口大小加1。如果存在该元素,则从set集合中移除,窗口左端移动,窗口大小减1。最终使用maxLen存储最长子串长度。
代码展示:
public class _3_无重复字符的最长子串 {
public static void main(String[] args) {
System.out.println(lengthOfLongestSubstring("abcabcbb"));
}
public static int lengthOfLongestSubstring(String s) {
//存储不重复子串
Set<Character> set = new HashSet<>();
int len = 0, maxLen = 0;
//[left,right]滑动窗口 len为窗口大小
int left = 0, right = 0;
while (right < s.length()) {
if (!set.contains(s.charAt(right))) { //set中不存在char[right],添加到set中,right右移,len++
set.add(s.charAt(right));
right++;
len++;
}else { //set中存在char[right],移除char[left],left右移,len--
set.remove(s.charAt(left));
left++;
len--;
}
maxLen = Math.max(len,maxLen);
}
return maxLen;
}
}
LeetCode209:长度最小的子数组
题目描述:
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0 。
题解:
本题可以使用滑动窗口求解。[left, right]表示滑动窗口范围,minLen记录最小子数组长度,sum记录子数组的总和。通过移动窗口右端right,增加sum,当sum >= target时,记录当前窗口大小,并更新最优解, 然后将left尝试向右边移动,直到sum<target为止, 这样就能得到滑动窗口中最小长度。
代码展示:
public class _209_长度最小的子数组 {
public static void main(String[] args) {
int[] nums = {2, 3, 1, 2, 4, 3};
System.out.println(minSubArrayLen(7,nums));
}
public static int minSubArrayLen(int target, int[] nums) {
//窗口区间[left,right]
int left = 0;
int right = 0;
int minLen = Integer.MAX_VALUE; //最小滑动窗口的长度
int sum = 0; //滑动窗口数值之和
while (right < nums.length) {
sum += nums[right]; //窗口扩大,加入right对应元素,更新当前结果
while (sum >= target) { //结果满足的条件
//更新最优解
minLen = Math.min(minLen,right - left + 1)
//窗口缩小,移除left对应元素,left右移
sum -= nums[left];
left++;
}
right++;
}
return minLen;
}
}