什么是双指针?
(对撞指针、快慢指针)
双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算。
对撞指针
对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。
对撞数组适用于 有序数组,也就是说当你遇到题目给定有序数组时,应该第一时间想到用对撞指针解题。
伪代码大致如下:
function fn (list) {
var left = 0;
var right = list.length - 1;
//遍历数组
while (left <= right) {
left++;
// 一些条件判断 和处理
... ...
right--;
}
}
例子:救生艇问题
给定数组 people 。people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。
每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。
返回 承载所有人所需的最小船数 。
要使需要的船数尽可能地少,应当使载两人的船尽可能地多。
设 people 的长度为 n。
考虑体重最轻的人:
若他不能与体重最重的人同乘一艘船,那么体重最重的人无法与任何人同乘一艘船,此时应单独分配一艘船给体重最重的人。从 people 中去掉体重最重的人后,我们缩小了问题的规模,变成求解剩余n−1 个人所需的最小船数,将其加一即为原问题的答案。
若他能与体重最重的人同乘一艘船,那么他能与其余任何人同乘一艘船,为了尽可能地利用船的承载重量,选择与体重最重的人同乘一艘船是最优的。从 people中去掉体重最轻和体重最重的人后,我们缩小了问题的规模,变成求解剩余 n−2 个人所需的最小船数,将其加一即为原问题的答案。
在代码实现时,我们可以先对 people 排序,然后用两个指针分别指向体重最轻和体重最重的人,按照上述规则来移动指针,并统计答案。
class Solution {
public:
int numRescueBoats(vector<int>& people, int limit) {
int ans=0;//记录船数
sort(people.begin(),people.end());//先排序
int light=0,heavy=people.size()-1;
while(light<=heavy)
{
if(people[light]+people[heavy]>limit)
{
--heavy;//heavy加上一个最小的都会超出限制,直接自己一个人坐船 ,ans++
}else{//两个人可以一起坐船,ans++
++light;//两个人已经坐上了船,移动指针
--heavy;
}
ans++;
}
return ans;
}
};
快慢指针
快慢指针也是双指针,但是两个指针从同一侧开始遍历数组,将这两个指针分别定义为快指针(fast)和慢指针(slow),两个指针以不同的策略移动,直到两个指针的值相等(或其他特殊条件)为止,如fast每次增长两个,slow每次增长一个。
例子:环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
Floyd判圈算法(龟兔赛跑算法) - 简书 (jianshu.com)
假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。
当「乌龟」和「兔子」从链表上的同一个节点开始移动时,
如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;
如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。
等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,
即套了「乌龟」若干圈。
class Solution
{
public:
bool hasCycle(ListNode *head)
{
ListNode *fast = head;
ListNode *slow = head;
while (fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
{
return true;
}
}
return false;
}
};
总结
当遇到有序数组时,应该优先想到双指针来解决问题,因两个指针的同时遍历会减少空间复杂度和时间复杂度。
练手
1.盛最多水的容器
盛最多水的容器 - 盛最多水的容器 - 力扣(LeetCode)
直接看题解就行,肯定比我写得好。
就是说学习双指针做法
class Solution {
public:
int maxArea(vector<int>& height) {
int l=0,r=height.size()-1; //双指针
int ans=0;
while(l<r)
{
int area=min(height[l],height[r])*(r-l);
ans=max(ans,area);//ans记录目前最大面积
if(height[l]<=height[r])//小的指针进行移动
{
++l;
}else{
--r;
}
}
return ans;
}
};
2.三数之和
最接近的三数之和 - 最接近的三数之和 - 力扣(LeetCode)
给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。
返回这三个数的和。
假定每组输入只存在恰好一个解。
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int n = nums.size();
int best = 1e7;
// 枚举 a
for (int i = 0; i < n; ++i) {
// 保证和上一次枚举的元素不相等
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 使用双指针枚举 b 和 c
int j = i + 1, k = n - 1;
while (j < k) {
int sum = nums[i] + nums[j] + nums[k];
// 如果和为 target 直接返回答案
if (sum == target) {
return target;
}
if (abs(sum - target) < abs(best - target)) {
best = sum;
}
if (sum > target) {
// 如果和大于 target,移动 c 对应的指针
int k0 = k - 1;
// 移动到下一个不相等的元素
while (j < k0 && nums[k0] == nums[k]) {
--k0;
}
k = k0;
} else {
// 如果和小于 target,移动 b 对应的指针
int j0 = j + 1;
// 移动到下一个不相等的元素
while (j0 < k && nums[j0] == nums[j]) {
++j0;
}
j = j0;
}
}
}
return best;
}
};
3.移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
解题思路
两次遍历
我们创建两个指针 i 和 j,第一次遍历的时候指针 j 用来记录当前有多少 非0 元素。即遍历的时候每遇到一个 非0 元素就将其往数组左边挪,第一次遍历完后,j 指针的下标就指向了最后一个 非0 元素下标。
第二次遍历的时候,起始位置就从 j 开始到结束,将剩下的这段区域内的元素全部置为 0。
动画演示:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n=nums.size();
int a=0; int b=0;
for(a=0;a<n;a++)
{//b从0位置开始,只要a的值不为0,nums[b]=nums[a];
if(nums[a]!=0)
{
nums[b]=nums[a];
b++;
}
}//循环结束b记录最后一个不为0的数,则后面全为0
for(int i=b;i<n;i++)
{
nums[i]=0;
}
}
};
快慢指针找链表的中间节点