公众号:自学编程村,回复“书籍”,领取1000多本计算机书籍。涵盖语言语法、数据结构算法、AI相关书籍、开发相关等等各种书籍,应有尽有,你总会用到。
回复“C语言”,领取原创C语言九讲PDF教程(数万字、百图)。
回复学习路线,领取C++和Java学习路线和资源推荐总总结。
这篇文章终于来了哈哈,为大家带来LeetCode Hot100中双指针内容我的总结。
目录
序
LeetCode 238 移动零
LeetCode 11 盛水最多的容器
LeetCode 15 三数之和
LeetCode 42 接雨水
序
我们本节为大家带来双指针内容的总结。
看完本篇文章呢,你将会了解双指针的基本使用方法,会初步体会到双指针 是如何使用的,以及我们在平时做题中,在什么样的场景下应该去想到用双指针。
看完本篇文章,你将收获一道easy,两道mid和一道hard。并且对于hard接雨水的题目,我们会给出两种算法,并详细解答为什么(官方题解似乎只把过程阐述清楚了,而对于原因阐述较少)。
其实双指针思想之深邃,远不止此,大家可以不用拘泥于Hot100上的这几道题,可以再LeetCode的专题中再去找一些别的题来更深一层次地体会双指针的使用之处。
我们先来说说双指针的模板是怎么样的?所谓模板就是板子,一般情况下的算法步骤。
void two_pionter()
{
for(int i = 0; i < n; i++)
{
int j = i;
while(judge( )) j++;
}
}
注意,上面的代码只是一种思想,实际上的代码并不一定会和其完全一样。
我认为,它实际上和贪心、DP等是一样子的,只存在思想,而没有说有非常通用的模板。我们需要在平常的练习中多多地总结和思考。
LeetCode 238 移动零
我们先来说238题,移动零。
283. 移动零 - 力扣(LeetCode)
这道题实际上很简单。但是乍一看好像有的同学也会被男主hhh。
这道题要想明白一个点,就会比较容易了。那就是:移动到数组后面的数,都是0。并不是其他不同的数。也就是说,移动到数组后面的数都是相同的!既然是相同的,那就好办了。
所以,我们就可以:把前面所有的数都移动到前面去之后,把后面的数手动设置成0,就好了。不需要考虑0被我们覆盖掉的问题,因为我们知道,我们覆盖掉的都是0。最后移动完之后,手动把数组后面所有的元素都变成0就可以了。
那这样以后,代码就比较简单了。下面是一个我自己写的示例代码:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int fast = 0, slow = 0;
for(; fast < nums.size(); fast++){
if(nums[fast] != 0) nums[slow++] = nums[fast];
}
for(; slow < nums.size(); slow++)nums[slow] = 0;
}
};
这道题也是双指针一道比较简单的应用。
LeetCode 11 盛水最多的容器
我们接下来来看第二题。
11. 盛最多水的容器 - 力扣(LeetCode)
第二题,需要想明白一个点。想明白这个点之后,问题就会变得简单。
需要想明白什么呢?实际上是一种贪心的思路。或者说,它是完全搜索(就是两次O(N^2)搜索的剪枝)
就是拿两个双指针放在两端,分别往中间走,每次走的是双指针指向的两个柱子中,矮的那个柱子。这样就一定能找得到最大的盛水容器。
那为什么是这样呢?
首先,根据局部贪心,我们肯定优先走短的,而不是长的。那为什么走短的,最后就一定可以找到最大的而不会被漏掉呢?
因为一旦被遍历到,那肯定就能找到最大的。因为我们是在所有的容器中找最大值。那问题就是最大值有没有可能没有被遍历到呢?
我们来假设双指针指向的两个柱子分别是左边的和右边的柱子。并且此时,左边的柱子是高的,右边的柱子是矮的。我们分别用红色的矩阵来表示,并在图上写上了高和矮。
那么,我们是要移动矮的柱子,所以矮的柱子的右边情况是都可以考虑到的,我们现在要考虑的是被剪枝优化掉的情况,也就是左边高的的柱子的右侧的三种情况。
我们分别用1,2,3来去表示三种情况。
1、比左边的柱子矮,但是呢比右边的红色柱子高。
2、比左边的柱子矮,并且比右边的柱子矮。
3、比左边的柱子高。
这三种情况的分析如图所示。
所以说,这三种情况我们是都不用考虑的。都不会比当前的容器的值要大。所以我们不用移动高的柱子,只要移动矮的柱子就可以了。
想明白这个点,这个问题就比较简单了。
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0, j = height.size() - 1;
int ans = 0;
while(i < j){
ans = max(ans, min(height[i], height[j]) * (j - i));
if(height[i] > height[j]) j--;
else i++;
}
return ans;
}
};
LeetCode 15 三数之和
继续来看第三题。第三题比较简单。
15. 三数之和 - 力扣(LeetCode)
为什么说比较简单呢?
因为要求三数之和为0,和求两数之和差不多。因为要求三数之和,我们只要枚举第一个数,然后让第二个数和第三个数的和等于第一个数的负数就可以了。后面的操作(即让第二个数和第三个数的和等于第一个数的负数)我们可以用双指针来去做。
这道题很简单。但是要注意数字可能会重复,所以要去重。
下面是代码:
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++)
{
if(i && nums[i] == nums[i-1]) continue;
int k = nums.size() - 1;
//即使用双指针 j, k 来去寻找 nums[j] + nums[k] = -nums[i]的,不可以包含重复的,且无序
for(int j = i + 1; j < nums.size(); j++)
{
if(j != i + 1 && nums[j] == nums[j - 1]) continue;
while(j < k && nums[j] + nums[k] > -1 * nums[i])
{
k--;
}
if(nums[j] + nums[k] == -1 * nums[i]) {
if(j == i || j == k || i == k) continue;
ans.push_back({nums[i], nums[j], nums[k]});
}
}
}
return ans;
}
};
好,我们最后来看接雨水。
LeetCode 42 接雨水
42. 接雨水 - 力扣(LeetCode)
这道题我们会说两种做法(虽然后一种是前一种的优化,但也是后一种思想的基础),并且会详细解答为什么。
接雨水这道题,和双指针有关系,但是也没有那么大的关系。我觉得它更加类似于一个伪单调栈的问题。
这道题首先要搞明白雨水是怎么样被接的。如果你能把它描述出来,那么也基本上就可以做出来这道题了。
这里我的描述方式是:对于每一个点的位置,取该点左侧所有点的最大值和右侧所有点高度的最大值,然后这两个高度的最大值是决定在该位置能装多少雨水的关键。再根据大家都知道的木桶原理,再取这两个最大值的最小值,该最小值就决定了在该位置能装多少水。
举个简单的例子,对于该题的height[5],先找到它的左侧最大值是2,右侧最大值是3。然后2和3里面最小值是2,2再减去height[5],即2 - 0 = 2,为该位置(即height[5])的地方能装雨水的最大值。
其实如果只是要做出来,没有空间复杂度的要求,还是比较容易的。因为对于上面的思路,我们可以两个数组分别来存储每个点左侧的最大值和右侧的最大值。这样我们可以在O(1)的时间复杂度找到每个点两侧的最大值。然后找到最大值之后,再进行相减的操作即可。
如下是一段示例代码:
class Solution {
public:
// 空间复杂度为O(N),时间复杂度O(N)
//开辟两个数组a,b,用来分别存储每个位置左边的最大值和右边的最大值
int a[20000 + 5], b[20000 + 5];
int trap(vector<int>& height) {
int ans = 0;
int left = 0, right = height.size() - 1;
//初始时为0
int left_max = 0;
for(int i = 0;i < height.size(); i++)
{
a[i] = left_max;
left_max = max(left_max, height[i]);
}
//初始时为0
int right_max = 0;
for(int i = height.size() - 1; i >= 0; i--)
{
b[i] = right_max;
right_max = max(right_max, height[i]);
}
//然后左右选最小的,减去当前的,算当前的乘雨水的值
for(int i = 0; i < height.size(); i++)
{
int CanSave = min(a[i], b[i]);
int cur = max(CanSave - height[i], 0);
ans += cur;
}
return ans;
}
};
那么官方的解法,让两个指针从两边往中间走,把O(N)的空间复杂度优化到了O(1)。实际上,这还是一种基于贪心的思想。
在LeetCode官方解答中并没有说明为什么,只是把这种做法解释清楚了。我们这里再去解释下为什么。
两个指针从右往左走,感觉和第二题又有点相似了。
思路是:两个指针,开始时在两边,让其分别往中间走。每次走的都是矮的那个。
然后维护两个变量RightMax和LeftMax(即右侧最大值和左侧最大值,每次右侧指针往中间走时,更新维护RightMax,左侧指针往中间走时,更新维护LeftMax),分别表示当前右侧的最大值和当前左侧的最大值。
那为什么可以这么做呢?我们都说,贪心的验证靠提交,那一种说法就是这么做一提交ac了哈哈,那自然这么做就是可以的。
那如果要解释,这种做法为什么可以呢?
首先,我们还是得要用和上一题类似的思想,以每个点的能够接到的雨水量为观察点。
那每个点能够接到的雨水量都是取决于 该点左侧最大值和右侧最大值 的最小值。(我们在刚刚的O(N)的空间复杂度里已经分析过)
(图思路分析)
我们假设一种情况吧,假如此时左边的矮,那左边的指针要往中间走,然后此时要计算的是左边Left指针位置能够接到的雨水量。
计算Left指针指向的点的(我们假设为i)雨水量的公式:min(左侧最大值,右侧最大值) - height[i]。
那左侧最大值就是LeftMax,我们现在要分析右侧最大值是不是RightMax呢?
左侧最大值 = LeftMax;
右侧最大值 = ?
因为在Right指针右边,是不可能有数超过RightMax的,所以我们只需要分析Right指针左边的即可,也就是分析[Left, Right]两个指针之间的数。
假如说,在中间存在有三种情况:
1、比height[Right]高。
2、如果比height[Right]矮,但是比height[Left]高。
3、比height[Left]还要矮。
讨论情况如下:
仍然分成三种情况讨论:
1、比height[Right]高,由于木桶原理,取决于能接多少雨水的是矮的柱子,而height[Left] < height[Right] < height[1],所以,1的情况并不会改变Left位置的接雨水的量。
2、如果比height[Right]矮,但是比height[Left]高。同1。
3、比height[Left]还要矮。由于站在Left的位置处考虑,RightMax不变(意思是,Left位置接雨水的量取决于min(左侧最大值,右侧最大值),但此时右侧最大值的没有改变的,依旧是RightMax),所以此时计算结果也不会变。
因此,我们如果使用RightMax这个值作为右侧最大值,这三种情况都不会改变Left指针指向的点的(我们假设为i)雨水量。因此,我们可以用RightMax来替代那个右侧最大值。
所以,我们的思路分析就结束了,这种方法可行。具体思路步骤参考【图思路分析】
我们最后给出我们的代码:
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
int left = 0, right = height.size() - 1;
int LeftMax = 0, RightMax = 0;
while(left < right)
{
//先更新LeftMax和RightMax
LeftMax = max(LeftMax, height[left]);
RightMax = max(RightMax, height[right]);
//每次走矮的
if(height[left] < height[right])
{
//走之前算出该点能够接的雨水的量,注意不可以小于0
ans += max(LeftMax - height[left], 0);
//然后往中间走
left ++;
}
else
{//同理即可
ans += max(0, RightMax - height[right]);
right --;
}
}
return ans;
}
};
可以看出,这个双指针,在这几道题中,往往会和贪心或者说剪枝(如果把枚举看成是暴力搜索)结合起来。它并不仅仅拘泥于快慢指针的题。还有像两个指针从两边走然后往中间相遇的问题,关键在于怎么走以及这么走能不能行、对不对的问题。这类问题说实话,我自认为没有什么好的模板,唯一的做法就是 多练习吧哈哈哈,当你见过了数种双指针的走法之后,你看到这些题目就自然而然地、或多或少的有点思路了。
好了,我是自学编程村村长,如果你想自学编程,欢迎大家关注微信公众号 自学编程村。
下一节,我们会跳一下,因为觉得前面这些题可能没什么模板,没有什么可讲性哈哈,下面一节将会为大家带来图和树的有关知识。