理解双指针
在Java中双指针主要用来遍历数组,两个指针指向数组中不同的元素,从而协同任务。也可以延伸到多个数组的多个指针。
快慢指针:两个指针指向同一数组,遍历方向相同,两个指针包围的区域称为滑动窗口,经常用于区间搜索。
左右指针:两个指针指向同一数组,遍历方向相反,可以用来进行搜索,被搜索的数组往往是排好序的。
左右指针
指针的概念结合实例更容易理解,下面结合力扣题目对指针进行说明:
题目描述
给出一个由元素从小到大排列的顺序数组numbers,同时给出一个整数target为该数组中两个不同的元素a和b的和,要求返回长度为2的整数数组,数组中元素为a、b在numbers中的下标,numbers数组的下标从1开始。
输入输出样例
Input : numbers = [0, 2, 4, 6, 7, 8], target = 9
Output : [2, 5]
题解
1、题目要求求出两个元素的下标,可以使用双指针定位两个元素;
2、定义左指针left在数组的最左侧,右指针right在数组的最右侧;
3、取数组元素numbers[left]和numbers[right]相加同target比较,若两数之和num恰好等于target,则将left + 1和right + 1放入数组中返回;若num小于target,则将left右移,使结果稍大一点;若num大于target,则将right左移,使结果稍小一点。
class Solution {
public int[] twoSum(int[] numbers, int target) {
int n = numbers.length;
int left = 0, right = n - 1;
while(left < right) {
int num = numbers[left] + numbers[right];
if(num == target) {
break;
}
if(num < target) {
left++;
}
if(num > target) {
right--;
}
}
return new int[] {left + 1, right + 1};
}
}
此例中双指针是如何避免其中一个指针会超过所求解的下标的呢?
可以证明,对于排好序且有解的数组,双指针一定能遍历到最优解。证明方法如下:假设最优解的两个数的位置分别是l和r。
我们假设在左指针在l左边的时候,右指针已经移动到了r;此时两个指针指向值的和小于给定值,因此左指针会一直右移直
到到达l。同理,如果我们假设在右指针在r右边的时候,左指针已经移动到了l;此时两个指针指向值的和大于给定值,因此
右指针会一直左移直到到达r。所以双指针在任何时候都不可能处于l和r之间,又因为不满足条件时指针必须移动一个,所以
最终一定会收敛在l和r。
题目描述
有两个有序数组nums1和nums2,请将nums2合并到nums1中,使nums1成为一个有序数组。
nums1和nums2的长度分别为m和n, 可以假设nums1的长度为m + n, 这样可以使nums1完全容纳nums2的元素。
输入输出样例
Input : nums1 = [1, 2, 3, 0, 0, 0], nums2 = [2, 4, 6], m = 3, n = 3
Output : nums1 = [1, 2, 2, 3, 4, 6]
题解
1、nums1和nums2是已经排好序的数组,合并数组时只需要比较nums1和nums2中元素的大小即可;
2、因为nums1的长度满足m + n,而且题目给定nums1和nums2的长度m和n, 可以考虑在两个数组的末尾m和n处分别定义指针,从后向前对数组中的元素进行比较;
3、两个数组中元素比较的较大值放到数组nums1的对应位置,这个位置也需要使用一个指针进行记录,初始位置为m + n - 1,也就是nums1数组的末尾;
4、如果nums2先遍历结束,那么nums1剩余元素是已经排好序的且和nums1之前的元素位置相同,不需要继续遍历;如果num1先遍历结束,则需要将nums2中未遍历完的元素放入nums1的剩余位置上;
5、代码中使用了- -操作符,举例来说:- -a的返回值为a - 1,而a- -的返回值为a.
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int pos = m-- + n-- - 1;
while(m >= 0 && n >= 0) {
nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];
}
while(n >= 0) {
nums1[pos--] = nums2[n--];
}
}
}
快慢指针
题目描述
给定一个链表head,判断其是否有环,无环返回null, 有环返回环的入点。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
输入输出示例
Input : head = [3,2,0,-4], pos = 1
Output : 返回索引为 1 的链表节点
题解
找环形链表入口的问题是经典的快慢指针的问题,分为以下步:
1、定义fast和slow两个指针,都从链表头部开始;
2、快指针一次走两步,慢指针一次走一步。如果链表存在环,两个指针都不可能结束,并且在某个时刻相遇;如果链表不存在环,快指针先结束;
3、两个指针相遇时,将快指针移动到链表头部,两个指针每次都走一步,再次相遇时即为链表环的入点,关于此处的证明大家可以看力扣官方题解的第二种快慢指针解法,之后我这里也会更新详细一点的解法。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null) {
return null;
}
ListNode fast = head, slow = head;
do {
if(fast.next == null || fast.next.next == null) {
return null;
}
fast = fast.next.next;
slow = slow.next;
} while(fast != slow);
fast = head;
while(fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
滑动窗口
题目描述
给定一个字符串s,一个字符串t,返回s中包含t的最小子字符串,如果s中没有包含t的子字符串,返回空字符串"".
输入输出示例
input : s = "ADOBECODEBANC", t = "ABC"
output : "BANC"
题解
此种问题可以使用“滑动窗口”求解
滑动窗口的基本思路是,定义左右两个指针,首先将右指针右移,同时判断从左指针到右指针的字符串是否包含t中所有字符,不包含,右指针继续右移,左指针不动,如果包含,则右指针停止右移,左指针右移,缩小左右指针中间包含的字符串的范围,寻找最小子字符串(同一时间内只有一个指针会进行移动)。
具体的做法为:
1、定义哈希表ori,用于存储t字符串中所有字符及其出现次数;定义哈希表cnt,用于动态存储左右指针之间的字符串所包含的字符及其出现的次数;
2、右移右指针,当右指针未到达右边界且ori包含右指针所标记的字符时,将该字符放到cnt并更新cnt;
3、当cnt包含所有ori中的字符且左指针left小于右指针right时,更新当前left到right的长度len,并更新当前左右指针的值到变量ansL和ansR中;
4、判断cnt中是否包含left所在的字符,如果包含,将该字符从cnt中去除,保证下一次循环中判断cnt包含所有ori中字符的准确性;
5、最后,如果ansL不是初始值-1,说明左右指针都进行过移动,s中有包含t中所有字符的子字符串,返回s.substring(ansL, ansR),否则返回空字符串"".
class Solution {
Map<Character, Integer> ori = new HashMap<>();
Map<Character, Integer> cnt = new HashMap<>();
public String minWindow(String s, String t) {
int tLength = t.length();
for(int i = 0; i < tLength; ++i) {
char c = t.charAt(i);
ori.put(c, ori.getOrDefault(c, 0) + 1);
}
int left = 0, right = -1, sLength = s.length(), ansL = 0, ansR = 0, len = Integer.MAX_VALUE;
while(right < sLength) {
++right;
// right指针右移,如果有包含t中的字符就将该字符放到动态的cnt中
if(right < sLength && ori.containsKey(s.charAt(right))) {
cnt.put(s.charAt(right), cnt.getOrDefault(s.charAt(right), 0) + 1);
}
// 当cnt包含t中所有字符时,进行left指针左移的操作
// check()方法避免了当cnt不再包含t中所有字符时left指针继续左移的情况
while(check() && left <= right) {
// 此处判断避免了当right指针继续右移的时候会取到比之前的len更长的子字符串的情况,保护了ansL和ansR的值
if(right - left < len) {
len = right - left + 1;
ansL = left;
ansR = left + len;
}
if(cnt.containsKey(s.charAt(left))) {
cnt.put(s.charAt(left), cnt.getOrDefault(s.charAt(left), 0) - 1);
}
++left;
}
}
return ansL == -1 ? "" : s.substring(ansL, ansR);
}
/**
检查cnt中是否包含所有ori中的key值(也就是t中包含的所有字符)
*/
private boolean check() {
Iterator iter = ori.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
Character key = (Character)entry.getKey();
Integer value = (Integer)entry.getValue();
if(cnt.getOrDefault(key, 0) < value) {
return false;
}
}
return true;
}
}