双指针用法梳理
文章目录
1.问题引入
以LeetCode的每日一题:剑指 Offer 52. 两个链表的第一个公共节点为例引入双指针算法。再对双指针算法进行后续的分析总结。
1.1 问题描述
输入两个链表,找出它们的第一个公共节点。
如下面的两个链表:
在节点 c1 开始相交。
1.2 示例
注意:
- 如果两个链表没有交点,返回 null.
- 在返回结果后,两个链表仍须保持原有的结构。
- 可假定整个链表结构中没有循环。
- 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
- 本题与主站 160 题相同:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/
1.3 问题分析
对于这题就可以采用双指针的思想解决。
首先两个链表有公共节点的前提是两个链表都必须是非空,若一个为空,直接返回NULL:if(headA == NULL||headB == NULL) return NULL
在headA和headB都不为空的前提下我们才能进一步判断是否有公共的节点。做法如下:
创建两个指针ListNode* tempA = headA, ListNode* tempB = headB
.
- 每一步操作时同时更新
tempA
和tempB
- 对于每一步的更新需要做的是:如果
tempA!=NULL
,那么tempA = tempA->next
,指针沿着链表后移;如果tempB!=NULL
,tempB = tempB->next
,指针也跟着后移。 - 若
tempA==NULL
,即移动到A的末尾了,那么tempA = headB
,将tempA
移动链表headB
的头部。若tempB==NULL
,即移动到B的末尾了,那么tempB = headA
,将tempB
移动到链表headA
的头部 - 当
tempA == tempB
时,返回他们的节点。
- 对于每一步的更新需要做的是:如果
下面我们要说明的就是为什么经过上面的操作,返回的就一定是公共的节点(如果不含公共部分那么返回的是Null)
证明:
-
情况一:两个链表相交
设链表A长度为m,链表B长度为n,我们设A不与B公共的部分的长度为a个节点(在图中a=3),B不与A公共的部分的长度为b个节点(图中b=1),同时设A与B公共部分的长度为c(图中c=2)。那么有:a+c=m,b+c=n。
-
如果 a=b,则两个指针会同时到达两个链表的第一个公共节点,此时返回两个链表的第一个公共节点;
如果 a≠b,则指针tempA
会遍历完链表 headA,指针 tempB
会遍历完链表headB,两个指针不会同时到达链表的尾节点,然后指针 tempA
移到链表headB 的头节点,指针tempB
移到链表headA 的头节点,然后两个指针继续移动,在指针 tempA
移动了 a+c+b 次、指针 tempB
移动了 b+c+a 次之后,两个指针会同时到达两个链表的第一个公共节点,该节点也是两个指针第一次同时指向的节 点,此时返回两个链表的第一个公共节点。
-
情况二:两个链表不相交
还是保持上述的假设,A链表长为m,B链表长为n,如果m==n,那么二者同时到达尾部,返回NULL
如果m≠n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针
tempA
移动了 m+n次、指针tempB
移动了 n+m 次之后,两个指针会同时变成空值NULL
,此时返回NULL
这就是对于上述的问题,使用双指针的解法。有效将时间复杂度避免成O( n 2 n^2 n2)
程序如下:
//双指针法
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA == NULL ||headB == NULL) return NULL; //如果有一个是空的,那么直接返回NULL
ListNode *tempA = headA, *tempB = headB;
while(tempA!=tempB){ //两个指针比较就是比较他们指向的地址是否相同
tempA = tempA!=NULL?tempA->next:headB;
tempB = tempB!=NULL?tempB->next:headA;
}
return tempA;
}
};
2.双指针的其他用法
双指针是一种思想,在链表中运用较多,主要还有下面几种场合:
- 快慢指针
- 链表中点的计算
- 判断链表中是否有环
- 判断链表中环的起点
- 求带环链表中环的长度
- 求链表倒数第k个元素
- 碰撞指针
- 二分查找
- n数之和
- 滑动窗口
- 字符串匹配
- 子数组
2.1 快慢指针
2.1.1计算链表的中点
- 问题描述
给定一个头结点为 head
的非空单链表,返回链表的中间结点。ps:如果有两个中间结点,则返回第二个中间结点。
- 解法
双指针。其中一个是快指针:每次操作走两步,即quick = quick->next->next
;另一个是慢指针:每次操作走一步,即slow = slow->next
。等到快指针走到链表末尾时,慢指针刚好到链表中点。
程序如下:
class Solution{
public:
ListNode* middleNode(ListNode* head) {
ListNode *quickHead = head, *slowHead = head;
while(quickHead != NULL){
quickHead = quickHead->next->next;
slowHead = slowHead->next;
}
return slowHead;
}
};
2.1.2 环形链表
-
问题描述
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
-
问题分析
对于环形链表,一样利用快慢指针即可,如果快指针又再次和慢指针相遇那么链表含环。如果快指针直接走到了null,那么不含环。
代码如下:
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *quick = head, *slow = head;
while(quick!=NULL&&quick->next!=NULL){
quick=quick->next->next;
slow = slow->next;
if(quick == slow){
return true;
}
}
return false;
}
};
2.1.3 带环链表的环长
-
问题描述
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:
你是否可以使用 O(1) 空间解决此题?
-
问题分析
对于这个问题也是使用双指针来解决。具体的分析请见另一篇博客:
代码如下:
class Solition{
public:
ListNode *detectCycle(ListNode *head) {
if(head == NULL){
return NULL;
}
ListNode *fast, *slow;
fast = slow = head;
while(true){
if(fast->next == NULL){
return NULL;
}
fast = fast->next->next;
slow = slow->next;
if( fast==NULL || slow == NULL ){ //如果根本没有环,那么结束程序,返回NULL
return NULL;
}
if(fast == slow){ //指针相遇,跳出循环
break;
}
}
fast = head;
while(fast!=slow){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
2.1.4 求链表倒数第k个元素
-
问题描述
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
-
问题分析
倒数第k个节点和寻找链表的中点的思路是相似的,这里是使快指针比慢指针先走k步,之后保持fast和slow指针同步前进。当fast指针到达末尾时,slow指针刚好到达链表的倒数第k个节点处。
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode *fast, *slow;
fast = slow = head;
int cnt = k; //计数
while(cnt--){ //让快指针先走k步
if(fast == NULL){
return NULL;
}
fast = fast->next;
}
while(fast!=NULL){
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
2.2 碰撞指针
2.2.1 二分查找
对于碰撞指针,二分查找是一种典型的碰撞的思想。这里二分查找不再介绍,直接介绍下一个n数之和。
2.2.2 n数之和
2.3滑窗法
2.3.1字符串匹配
- 问题描述
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度
- 问题分析
对于最长子串,最长数组等问题,常用的就是双指针。对于KMP熟悉的话也能想到这一点。
思路:使用两个指针left
和right
,初始值都设定为字符串的初始位置,即left = right = 0
。right
向前移动,每向前一步,判断[left, right]
区间内有没有和str[left]
重复的字符,若有重复的字符,首先记录下此时的right - left
为max_len
,并则将right
移动到区间内的重复字符的下一个位置。然后重复上述right
向前移动并判重的工作。具体步骤如下图所示的①②③④。之后重复②③④直到right == str[str.size()-1]
。此时的max就是最终的答案。
在判重的时候这里使用了map,map<char, int>
,将字符与他的下标联系起来存储。需要注意的就是移动left
时注意将变化的区间内的字符从map
中移出来。
程序如下:
#include<iostream>
#include<algorithm>
#include<map>
using namespace std;
class Solution{
public:
int lengthOfLongestSubstring(string s) {
if(s.size()==0){
return 0;
}
int left,right;
left = right = 0;
int max = 1;
map<char, int> char2int;
while(right<=s.size()-1&&left<=right){
cout<<"left: "<<left<<"right: "<<right<<endl;
if(char2int.count(s[right]) == 0){ //如果字符不在map中,说明没有重复字符
char2int[s[right]] = right; //那么加入map,和他的下标 map['s'] = right;
}else{//说明char2int[right]已经在map内
max = max > (right - left) ? max : right-left; //更新max
cout<<"max: "<<max<<endl;
for(int i = left; i < char2int[s[right]]; i++){
char2int.erase(s[i]);
}
left = char2int[s[right]] + 1; //重新移动left到重复字符的下一个位置
char2int[s[right]] = right; //更新新的下标
right++;
continue;
}
if(right == s.size()-1){
max = max > (right - left + 1) ? max : right-left+1;
}
right++;
}
return max;
}
};
int main()
{
string str = "pwwekk";
Solution s;
cout<<s.lengthOfLongestSubstring(str);
return 0;
}
2.3.2子数组
子数组问题常用的就是双指针。主要是判断出控制指针移动的条件,情况和对应的题目都比较多,慢慢补充。
给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
请你找出符合题意的最短子数组,并输出它的长度。
- 问题分析
题目要求找到最短的非升序子数组,并且是仅仅对这个子数组进行排序就能达到整个数组的有序。
我们可以使用两个指针:left = 0,right = nums.size()-1
,两个指针相向而行:left++,right--
。找到左端和右端的第一个不满足递增要求的数。假如有下面的数组:vector<int> vec = {2,7,3,1,10,6,8,9}
我们使用left从左向右搜寻,所谓不满足递增要求,就是这个数的下一个数比这个数小。第一个不满足这样的要求的数是7(它的下一个数是3,破坏了递增性)。我们记录left = 1,
即num[left] = 7
。
对于right
的搜索道理一样,我们这时候把右端看做起始端,要找的就是从右向左第一个不满足递减要求的数。即若这个数的下一个数(下一个数是从右向左看的下一个数,即坐标比它小1的数)破坏了递减性质,那么记录这个点为right
。在上图中,我们记录right = 5
,即nums[right] = 6
。
这样我们就得到了第一次搜索的一个区间:[left = 1, right = 5]
,即上图所示的红色区域。这个区间一定是要进行重排的数组。但是是否是对这个数组进行重排后整个数组就变得有序呢?答案是否的。因为这个区间外还有数字2比区间内的min值1大,还有数字8和9比区间内的最大值max小。仅仅对红色区域的数组进行排序是不够的,我们要在left
以左和right
以右对数组进行扩展。
而扩展这个区间的原则如下:
先记录红色区间内的最大值max和红色区间内的最小值min,由于left是第一个违背递增原则的数字,所以left以左是严格递增的。left
向左移动,找到最小的比min大的元素时停止。这里就是数字2,即left = 0
。这样我们确定了左边的最大边界。
我们再确定右边的最大边界:同理right
向右边移动,由于确定right时,right也是第一个违背规则的数字,所以right
右边也是递增的。right
不断向右移动,直到找到最大的小于max的元素时停止,这里就是数字9,即right = 7 ,nums[right] = 9
。这样我们确定了右边的最大边界。
综上所述:我们得到了扩展后的边界:[left = 0,right = 7]
,即区域[0,7]
就是最短无序连续子数组。
程序如下:
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
//两个指针,一个从左向右遍历,一个右向左遍历
int left = 0, right = nums.size() - 1;
for(int i = 0; i<nums.size()-1&&nums[i]<=nums[i+1]; i++) left++; //记录左边开始,第一个违反递增的元素
for(int j = nums.size()-1; j>=1&&nums[j]>=nums[j-1]; j--) right--; //记录从右边开始,第一个违犯递减的元素
if(left>=right){
return 0;
}
//找到区间内的最大值和最小值 [两个函数符合左闭右开原则]
int max_num = *max_element(nums.begin()+left,nums.begin()+right+1);
int min_num = *min_element(nums.begin()+left,nums.begin()+right+1);
//先扩展左边的区间,找到小于等于min_num的元素。返回其下标。
//在扩展区间的过程中还要更新max元素。
//同理扩展右边的区间,找到大于等于max_num的元素。返回其下标
while(left>=0&&nums[left]>min_num){
left--;
}
left++;
while(right<nums.size()&&nums[right]<max_num){
right++;
}
right--;
cout<<"left:"<<left<<" right:"<<right<<endl;
return right - left + 1;
}
};
注意到我们在进行左右边界的扩展的时候,并没有使用lower_bound(nums.begin(),nums.begin()+left, max_num)
的形式,而是使用了循环指针左移的形式。这是因为如果采用了lower_bound()
的形式,不仅非严格递增:即中间有相同数据的情况会出现程序错误,同时指针越界会造成一系列问题,所以使用while
循环解决问题。
元素的 频数 是该元素在一个数组中出现的次数。
给你一个整数数组 nums 和一个整数 k 。在一步操作中,你可以选择 nums 的一个下标,并将该下标对应元素的值增加 1 。
执行最多 k 次操作后,返回数组中最高频元素的 最大可能频数 。
- 问题分析
这个问题也被归纳到双指针中。主要的流程就是先排序,然后从nums
的末尾到开头双指针扫描,快指针只增不减,慢指针移动向前。程序如下:
class Solution {
public:
int maxFrequency(vector<int>& nums, int k){
sort(nums.begin(), nums.end());
int left, right;
left = right = nums.size()-1;
long sum = nums.at(nums.size()-1);
while(left>0){
left = left - 1;
sum += (long)nums.at(left);
if(sum + k >= (long)nums.at(right) * (right - left + 1)){
}
else{
sum = sum - nums.at(right);
right = right - 1;
}
}
return right - left + 1;
}
};