文章目录
双指针
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多 个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的 区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是 排好序的。
这里要注意一点,一旦遇到需要使用二重循环的问题,我们就可以思考一下,是否可以用双指针去做
一、两数之和
167-两数之和 II
题目
给定一个已按照 升序排列 的整数数组 numbers
,请你从数组中找出两个数满足相加之和等于目标数 target
。
函数应该以长度为 2
的整数数组的形式返回这两个数的下标值*。*numbers
的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length
。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers
按 递增顺序 排列-1000 <= target <= 1000
- 仅存在一个有效答案
思考
我们的第一反应,可能是使用二重循环去完成这道题目
但是,在一开始双指针的介绍中我们就说过,一旦遇到双重循环,我们就要考虑一下双指针的解法
题目中已经告诉我们,整个数组是已经排好序的,那我,我们可以让一个指针 l
指向数组头,一个指针 r
指向数组尾。如果两个位置的数字和小于 target
,则 l++
,如果两个位置的数字和大于 target
,则 r--
,如果以上两种情况都不是,说明找到了,返回下标(下标都要就+
1`后返回,因为题目要求从1 开始数)
题解
public int[] twoSum(int[] numbers, int target) {
int l=0,r=numbers.length-1;
while (l<r) {
if (numbers[l]+numbers[r]<target) l++;
else if (numbers[l]+numbers[r]>target) r--;
else return new int[]{++l,++r};
}
return null;
}
二、归并两个有序数组
使用双指针解法,可以将 O(n^2) 的复杂度,降为 O(N)
88-合并两个有序数组
题目
给你两个有序整数数组 nums1
和 nums2
,请你将 nums2
合并到 nums1
中*,*使 nums1
成为一个有序数组。
初始化 nums1
和 nums2
的元素数量分别为 m
和 n
。你可以假设 nums1
的空间大小等于 m + n
,这样它就有足够的空间保存来自 nums2
的元素。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[i] <= 109
思考
因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的 m − 1 位和 nums2 的 n − 1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。 因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。
在以下的代码里,我们直接利用 m 和 n 当作两个数组的指针,再额外创立一个 pos 指针,起 始位置为 nums1.length-1
。每次向前移动 m 或 n 的时候,也要向前移动 pos。
这里需要注意,如果 nums1 的数字已经复制完,不要忘记把 nums2 的数字继续复制;如果 nums2 的数字已经复制完,剩余 nums1 的数字不需要改变,因为它们已经被排好序。
题解
public void merge(int[] nums1, int m, int[] nums2, int n) {
m--;
n--;
int pos=nums1.length-1;
while (m>=0 && n>=0) {
if (nums1[m]>=nums2[n]) {
nums1[pos]=nums1[m];
m--;
} else {
nums1[pos]=nums2[n];
n--;
}
pos--;
}
/**
* num1[]已经排完了,但是num2[]没有排完
*/
if (m<0) {
while (pos>=0) {
nums1[pos]=nums2[n];
pos--;
n--;
}
}
}
283-移动零
题目
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
思考
我们一开始的想法,可能是遍历整个数组
一旦找到为 0 的元素,就将该 0 位置后面的所有元素整体前移一位
然后将最后一位改为0 ,直到数组遍历完成
使用上面的方法,时间复杂度为 O(n^2)
但是使用双指针,我们能一次遍历结束:
题解
public void moveZeroes(int[] nums) {
if(nums==null || nums.length<=1) {
return;
}
int i=0,j=0;
while (i<nums.length) {
if (nums[i]==0) {
i++;
} else {
if (i!=j) {
nums[j]=nums[i];
nums[i]=0;
}
i++;
j++;
}
}
}
代码优化:
上面那段代码,其实可以优化
通过观察我们可以发现,无论发生什么情况,i 每次都会向后移动一位,所以,我们可以用一个 for 循环,遍历 i 。
if(nums==null || nums.length<=1) {
return;
}
int j=0;
for (int i = 0; i < nums.length; i++) {
if (nums[i]!=0) {
if (i!=j) {
int tmp = nums[i];
nums[i]=nums[j];
nums[j]=tmp;
}
j++;
}
}
三、快慢指针
快慢指针常用在链表找环这类问题中
142-环形链表2
题目
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。注意,pos
仅仅是用于标识环的情况,并不会作为参数传递到函数中。
**说明:**不允许修改给定的链表。
进阶:
- 你是否可以使用
O(1)
空间解决此题?
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内 -105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
思考
关于链表找环,有一个固定套路,就是快慢指针
一个指针一次移动一步,一个指针一次移动两步,如果两个指针相遇,说明有环;如果快的指针指向为空,说明无环
但是,上面的步骤,只能判断有没有环,题目要求的是返回环的节点,这样,我们就要多出一步。一旦两个节点相遇,我们就将快节点放到链表头部,然后让两个节点同速,每次移动一位,当两个节点再次相遇的时候,相遇的位置,就是环的节点。
题解
public ListNode detectCycle(ListNode head) {
if(head==null || head.next==null || head.next.next==null) return null;
ListNode fast = head;
ListNode slow = head;
while (fast.next!=null && fast.next.next!=null) {
fast=fast.next.next;
slow=slow.next;
/**
* 两个指针相遇了
* 说明链表中有环的存在
*/
if (fast==slow) {
fast=head;
while (fast!=slow) {
fast=fast.next;
slow=slow.next;
}
return fast;
}
}
return null;
}
四、滑动窗口
76-最小覆盖子串
题目
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
**注意:**如果 s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
示例 2:
输入:s = "a", t = "a"
输出:"a"
提示:
1 <= s.length, t.length <= 105
s
和t
由英文字母组成
**进阶:**你能设计一个在 o(n)
时间内解决此问题的算法吗?
Related Topics
- 哈希表
- 字符串
- 滑动窗口
思考
滑动窗口的思想:
用i,j表示滑动窗口的左边界和右边界,通过改变i,j来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走,当这个窗口包含的元素满足条件,即包含字符串T的所有元素,记录下这个滑动窗口的长度j-i+1,这些长度中的最小值就是要求的结果。
步骤一
不断增加j使滑动窗口增大,直到窗口包含了T的所有元素
步骤二
不断增加i使滑动窗口缩小,因为是要求最小字串,所以将不必要的元素排除在外,使长度减小,直到碰到一个必须包含的元素,这个时候不能再扔了,再扔就不满足条件了,记录此时滑动窗口的长度,并保存最小值
步骤三
让i再增加一个位置,这个时候滑动窗口肯定不满足条件了,那么继续从步骤一开始执行,寻找新的满足条件的滑动窗口,如此反复,直到j超出了字符串S范围。
题解
这道题,就留给各位去思考一下吧
刚好作为前面学习的一个练习
小结
双指针有什么妙用?
- 快慢指针将链表折半
- 快慢指针判断链表环