题源acwing、力扣
讲解双指针
使用双指针是降低算法复杂度的一个有效途径,有些问题的暴力解法时间O(n2),但是使用双指针可以大幅度降低算法复杂度。
和贪心算法一样,双指针难在想不到
常用的双指针法有以下几类:
左右指针:两个指针,相向而走,中间相遇
快慢指针:两个指针,有快有慢,同向而行
灵活运用:两个指针,灵活运用,伺机而动
下面从例题里感受这三类情况
题目一二是左右指针
题目三四是快慢指针
题目一:盛最多水的容器
题源力扣11.点击跳转力扣
给定一个长度为 n 的整数数组 height 。
有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
思路
最优解法为左右双指针算法,双指针法的难点在于难以想到也难以证明,需要大量的刷题去练习题感
暴力法:
找出每一种情况,求出盛水值,最大的就是答案
- i 指左挡板,从第一块到遍历倒数第二块
- j 指右挡板,从倒数第一块遍历到 i 后面一块
- res 保存最大值,不断更新
- 返回 res
class Solution{
public:
int maxArea(vector<int>& height){
if(height.size() <= 1) return 0;
int res = 0;
for(int i = 0; i < height.size() - 1; i ++)//以 i 为左挡板,从 0 开始
for(int j = height.size() - 1; j > i; j --){
int l = j - i;//底边
int h = min(height[i], height[j]);
res = max(res, l * h);
}
return res;
}
}
答案正确,但是会超时,双指针算法还有可以优化的空间
优化:
假设S[l,r]为左挡板为l,右挡板为r的盛水值,那么我们有多少种情况?
再回顾一下,height数组[1,8,6,2,5,4,8,3,7]
暴力法里面,我们将所有情况都计算了一遍,实际上有很多情况不需要计算,因为一定不是答案
假设现在挡板是S[0,8],左高度是1,右高度是7,其中低一点高度的是1,现在移动右挡板还有意义吗?
没有意义
只要是向内移动右挡板,底边长一定变短,如果移动后高度比1高,就会变成底边长变短,高度不变,盛水量变小
只要是向内移动右挡板,底边长一定变短,如果移动后高度比1低,就会变成底边长变短,高度变低,盛水量变小
那么移动左挡板有意义吗?
有意义
只要是向内移动左挡板,底边长一定变短,移动后如果比1高,就会是底边长变短,高度变高,这种情况值得计算!
所以总结一下就是,每一种S[l,r]的情况下,计算后需要比较 l 和 r 的高度,如果height[l] < height[r]就执行 l ++
如果height[l] > height[r]就执行 r –
AC代码
class Solution {
public:
int maxArea(vector<int>& height) {
int l = 0;
int r = height.size() - 1;
int res = 0;
while(l < r){
res = max(min(height[l], height[r]) * (r - l), res);
if(height[l] < height[r]){
l ++;
}else{
r --;
}
}
return res;
}
};
题目二:查找总价格为目标值的两个商品
题源力扣179.点击跳转力扣
购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况,返回任一结果即可。
思路
题目很容易想到双指针,但是如果不留意冗余情况也容易超时
暴力法:
穷举每一种情况,如果两数之和等于target,就输出答案
- i 指向第一个数,从 0 到 price,size() - 2
- j 指向第二个数,从price.size() -1 到 i + 1
- 如果price[i] + price[j] == target,返回price[i], price[j]]
不贴代码了,因为我感觉没人会真的暴力吧,不会吧不会吧(罒ω罒)
优化
暴力法显然没有考虑到数组有序的条件
小时候听说过1+2+3+…+99,问有多少个100,巧妙算法咋算的?是不是头加尾,1 + 99, 2 + 98…
这题也是一样的
j 应该从price.size() - 1开始
不多说了,这题很容易想
AC代码
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
int j = price.size() - 1;
for(int i = 0; i < price.size() - 2; i ++){
while((price[i] + price[j]) > target) j --;
if(price[i] + price[j] == target) return vector<int>{
price[i], price[j]};
}
return {
-1, -1};
}
};
题目三:链表的中间节点
题源力扣876.点击跳转力扣
给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
这题是快慢指针,快慢指针是指移动的步长,即每次向前移动速度的快慢。(例如,让快指针每次前移动2步,慢指针每次向前移动1步)。
思路
暴力法:
- 用一个指针遍历一遍链表,求出链表的最后一个节点编号n
- 中间节点为n/2,向上取整
- 用一个指针从表头往后走n/2向上取整步,指向的节点就是答案
class Solution{
public:
ListNode* middleNode(ListNode* head) {
if(!head) return NULL;
int n = 0;
ListNode *p = head;
while(p->next){
n ++;
p = p->next;
}
int mid = (n + 1) / 2;
p = head;
while(mid --){
p = p->next;
}
return p;
}
}
暴力法可以AC,但是还有优化的空间
优化:
快慢指针,想象两个人跑步,一个人速度是另一个人的两倍,当速度快的到达终点的时候,另一个一定在中间,公式S=tv,t相同,v1 = 2*v2,那么路程也是两倍
AC代码
class Solution {
public:
ListNode* middleNode(ListNode* head) {
if(!head) return NULL;
ListNode *fast, *slow;
fast = head;
slow = head;
while(fast->next)
{
fast = fast->next;
if(fast->next) fast = fast->next;//fast走两步
slow = slow->next;//slow走一步
}
return slow;
}
};
题目四:环形链表
题源力扣141.点击跳转力扣
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
思路
暴力法
遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
具体地,使用哈希表来存储所有已经访问过的节点。每次到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到遍历完整个链表即可。
class Solution {
public:
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> h;//保存访问过的节点
while (head != nullptr) {
if (h.count(head)) {
//如果当前访问节点在h中,则有换
return true;
}
h.insert(head);//将当前节点放入h
head = head->next;
}
return false;
}
};
暴力法也能AC
优化
快慢指针做,一个人快一个人慢,如果有圈,两个人必然相遇,一个人跑两圈,第二个人跑一圈,会在起始位置相遇
AC代码
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head == NULL || head->next == NULL) return false;
ListNode *fast, *slow;
slow = head;
fast = head->next;
while(slow != fast){
if(fast == NULL || fast->next == NULL) return false;
fast = fast->next->next;
slow = slow->next;
}
return true;
}
};
题目五:环形链表II
题源力扣142.点击跳转力扣
思路
和I的区别就是如果有环需要返回入环第一个节点
其实这题我学过了
但是我又忘记了
啊啊啊小dream这次好好学,可不能再忘了o(╥﹏╥)o
现在假设slow和fast在圈内相遇了,slow走的路程是x+y,fast走的路程是x+y+n(z+y),其中n是圈数且大于等于1,因为fast速度是slow的2倍,所以它的节点数一定是slow的两倍,所以可以得到公式:x+y == n(z+y)。
现在需要知道环形的入口,所以需要知道x的长度,x = n(z+y)-y=(n-1)(z+y)+z
当n = 1的时候,x=z,意味着从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点。
当n > 1的时候,就是fast指针在环形转n圈之后才遇到 slow指针。其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,fast指针在环里 多转了(n-1)圈,然后再遇到slow,相遇点依然是环形的入口节点。
我有时候觉得编程就是数学(* ̄︶ ̄)
AC代码
class Solution {
public