一、双指针的简介
双指针是一种常用的编程技巧,用于在数组或链表中同时移动两个指针,以解决某些特定的问题。双指针通常用于寻找数组中的特定元素、反转链表、合并两个有序数组等场景。
双指针的基本思想是:使用两个指针(通常命名为left
和right
)从问题的两端开始向中间移动,直到它们相遇或满足某些条件。通过这种方式,双指针可以有效地处理一些需要从两个方向同时处理的问题。
二、双指针的分类
有人喜欢把双指针分为一下这么几种——
1)快慢指针
快慢指针,相信大多数的人和我一样,最早接触是在判断链表是否有环的时候接触到的,也就是下面这道题——
---------------------------------------------------------------------------------------------------------------------------------
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
力扣144:
环形链表
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1 输出:false 解释:链表中没有环。
---------------------------------------------------------------------------------------------------------------------------------
我觉得这就是一个非常经典的快慢指针的问题。初学的时候,看到这道题确实是一脸懵,不知道从哪里下手,其实如果是学过快慢指针,本题迎刃而解了。
想想看,假如说这个链表是有环的,那么我们让快指针一次走两步,慢指针一次走一步,那么这两个指针就都会走到环中,然后开始不断的循环,快指针走的快,总会追上慢指针(就像是跑步的时候,跑的慢的人会被跑的快的人套圈),我们只需要判断快慢指针是否可以相遇就可以得出这个链表是否有环。
有人可能有疑问了,如果没有环,会怎么样呢?没有环的话,fast会先走到终点——空(NULL),我们判断fast的状态就可以囊括这个情况了。
给出整体性的代码。
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* fast = head,*slow = head;
while(fast&&fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast)return true;
}
return false;
}
};
2)对撞指针
这个比较思想也比较好理解,我们也可以举一个例子,比如说
---------------------------------------------------------------------------------------------------------------------------------
蓝桥云课5375
---------------------------------------------------------------------------------------------------------------------------------
主要思想就是我们从两端想中间逼近,到达我们的某种目的,比如说反转的效果。这种双指针在学习的时候是很常见的,相信大家已经很熟悉了。
给出整体性的代码——
#include <iostream>
#include <string>
#include <cstdio>
#include <algorithm>
using namespace std;
int n, m;
const int N = 100010;
int arr[N];
void my_reverse(int left, int right)
{
while (left < right)
{
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
{
arr[i] = i;
}
while (m--)
{
int l, r;
scanf("%d%d", &l, &r);
my_reverse(l, r);
}
for (int i = 1; i <= n; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
3)分离指针
分离链表就是:两个指针分别属于不同的数组 / 链表。适合解决有序数组合并,求交集、并集问题。
再举一个例子——
———————————————————————————————————————————
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
力扣160.
相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为0
listA
- 第一个链表listB
- 第二个链表skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA
和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例 2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
———————————————————————————————————————————
题意很简单就是说让你看一下这两个链表是否有交集,也就是是否有公共的结点,如果有的话,就返回第一个公共的结点。
这个就很适合用我们的分离双指针,我给出整体性的代码,大家可以看一下。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* cur1 = headA,*cur2 = headB;
bool flag1 = true,flag2 = true;
while(cur1&&cur2)
{
if(cur1 == cur2)return cur1;
if(cur1)cur1 = cur1->next;
if(cur2)cur2 = cur2->next;
if(cur1 == NULL&&flag1){
flag1 = false;
cur1 = headB;
}
if(cur2 == NULL&&flag2)
{
flag2 = false;
cur2 = headA;
}
}
return NULL;
}
};
其实下边这个图可以很好的解释上边这一点代码,值得一提的是cur1和cur2都会在5之后的那个空姐点停留一下,然后才会返回另一个链表的头结点。
三、双指针经典题型的汇总
力扣283:
移动零
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums =[0,1,0,3,12]
输出:[1,3,12,0,0]
示例 2:
输入: nums =[0]
输出:[0]
其实题目中也给出了一个思路就是创建一个新的数组,把非零的数装入新的数组中,再把旧的数组更新一下就可以了,就像这样——
class Solution {
public:
void moveZeroes(vector<int>& nums) {
vector<int>result;
for(int i = 0;i<nums.size();i++)
{
if(nums[i])result.push_back(nums[i]);
}
for(int i = result.size();i<nums.size();i++)
{
result.push_back(0);
}
for(int i = 0;i<nums.size();i++)
{
nums[i] = result[i];
}
}
};
可以看出,效率也是很可观的,但是,题目中明确的指出,不让我们使用这个做法。
于是,我们采取另一种做法——双指针
我们的思路就是把整个数组给划分三个部分,一个是零区,一个是非零区,还有一个待定区。
给出一个整体性的代码,大家可以根据参考一下这个代码。
非常建议结合着例子进行分析(调试)。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = -1,right = 0;
while(right<nums.size())
{
if(nums[right]!=0)swap(nums[++left],nums[right]);
right++;
}
}
};
---------------------------------------------------------------------------------------------------------------------------------
力扣 1089
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
复写零
给你一个长度固定的整数数组 arr
,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。
注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。
示例 1:
输入:arr = [1,0,2,3,0,4,5,0] 输出:[1,0,0,2,3,0,0,4] 解释:调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]
示例 2:
输入:arr = [1,2,3] 输出:[1,2,3] 解释:调用函数后,输入的数组将被修改为:[1,2,3] 如果我们可以使用第二个数组,那么本题将会变得非常的简单,就像下边这样——
创建了一个数组:
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
vector<int>result;
int n = (int)arr.size();
for(int i = 0;i<n;i++)
{
if(result.size() == n)break;
if(arr[i] == 0)result.push_back(0);
result.push_back(arr[i]);
}
for(int i = 0;i<n;i++)
{
arr[i] = result[i];
}
}
};
原地修改:
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int left = 0,right = -1,n = (int)arr.size();
for(left = 0;left<n;left++)
{
if(arr[left])right++;
else right+=2;
if(right>=n-1)break;
}
if(right == n){
arr[n-1] = 0;
right -=2;
left--;
}
while(left>=0)
{
if(!arr[left]){
arr[right--] = 0;
}
arr[right--] = arr[left--];
}
}
};
做法就是我们找出那个修改之后的最后一个数,只考虑前边的数即可。
力扣202:快乐数
---------------------------------------------------------------------------------------------------------------------------------
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
示例 1:
输入:n = 19 输出:true 解释: 1^2 + 9^2 = 82 8^2 + 2^2 = 68 6^2 + 8^2 = 100 1^2 + 0^2 + 0^2 = 1
示例 2:
输入:n = 2 输出:false
提示:
1 <= n <= 2的31次方 - 1
刚开始看到这个题目的时候,可能是无从下手的。
那如果我告诉你这是一个快慢指针的问题,你会作何感想?
首先在计算的过程中,是一定会出现循环的(抽屉原理)。
假设我们给出的是9 9999 9999,计算一次之后得出810,也就是说我们题目所给出的范围一定在[1,810]之间,也就是说只要我们经过811次,就一定能保证有重复出现的,那不就是一个“环”了吗?
我们给出整体的代码:
class Solution {
private:
int count(int n)
{
int sum = 0;
while(n)
{
sum += (n%10)*(n%10);
n/=10;
}
return sum;
}
public:
bool isHappy(int n) {
int fast = count(n),slow = n;
while(fast != slow)
{
fast = count(fast);
slow = count(slow);
fast = count(fast);
}
if(fast == 1)return true;
else return false;
}
};
———————————————————————————————————————————
力扣11:盛最多水的容器
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1] 输出:1
看到这道题,很容易就能想到使用双指针的方法。我们使用左右两个指针,分别在整个数组的两端,计算其容纳水的多少,然后不断的更新结果。
整体性的代码如下:
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0,j = (int)height.size()-1,Max = min(height[i],height[j])*(j-i);
while(i<j)
{
if(height[i]<height[j])
{
i++;
Max = max(Max,min(height[i],height[j])*(j-i));
}
else{
j--;
Max = max(Max,min(height[i],height[j])*(j-i));
}
}
return Max;
}
};
———————————————————————————————————————————
力扣611:有效的三角形的个数
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
给定一个包含非负整数的数组 nums
,返回其中可以组成三角形三条边的三元组个数。
示例 1:
输入: nums = [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3
示例 2:
输入: nums = [4,2,3,4] 输出: 4
看到这道题,我一下子就想起来初中老师教过的三角形的判定定理:两个较小的边之和大于那个较大的边。因为这道题目我们数组的下标对我们涞水是没有意义的,而且我们又要找出较大的边和较小的边,所以我们第一步就直接进行排序。
排序之后。最后一个数就是我们的最大的数,然后我们让left从下标0开始往后,right从倒数第二个开始往前。
具体的代码如下:
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(),nums.end());
int n = (int)nums.size(),ret = 0;
for(int i = n-1;i>=2;i--)
{
int left = 0,right = i-1;
while(left<right)
{
if(nums[left] + nums[right] >nums[i])
{
ret += right-left;
right--;
}
else left++;
}
}
return ret;
}
};
———————————————————————————————————————————力扣15:三数之和
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
关于这道三数之和的问题,首先我们肯定是不难想到暴力的解法,那就是使用三重for循环,非常暴力的去解决问题,但是还需要考虑的是去重的操作(我觉得可以使用一个容器set来去重)。
来看一下双指针的解法:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
int n = nums.size();
vector<vector<int> > result;
for(int i = 0;i<n;)
{
if(nums[i]>0)break;
int left = i+1,right = n-1,target = -nums[i];
while(left<right)
{
int sum = nums[left]+nums[right];
if(sum>target)right--;
else if(sum<target)left++;
else{
result.push_back({nums[i],nums[left++],nums[right--]});
while(left<right&&nums[left] == nums[left-1])left++;
while(left<right&&nums[right] == nums[right+1])right--;
}
}
i++;
while(i<n&&nums[i]==nums[i-1])i++;
}
return result;
}
};
这种做法就是在遍历的同时进行了去重,效果明显。
———————————————————————————————————————————力扣12题:四数之和
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8 输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109
其实这个和三数之和的思路相同,我们可以把他化简成三数之和,两数之和,最后求得结果。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int> >result;
if(nums.size()<4)return result;
sort(nums.begin(),nums.end());
int n = (int)nums.size();
for(int i = 0;i<n;)
{
//三数之和
for(int j = i+1;j<n;)
{
//
long long x = (long long)target-nums[i]-nums[j];
int left = j+1,right = n-1;
while(left<right)
{
int sum = nums[left]+nums[right];
if(sum>x)right--;
else if(sum<x)left++;
else{
result.push_back({nums[i],nums[j],nums[left++],nums[right--]});
while(left<right&&nums[left] == nums[left-1])left++;
while(left<right&&nums[right] == nums[right+1])right--;
}
}
j++;
while(j<n&&nums[j] == nums[j-1])j++;
}
i++;
while(i<n&&nums[i] == nums[i-1])i++;
}
return result;
}
};