双指针算法
前言:
滑动窗口算法中经常会使用到双指针算法。如果想了解滑动窗口的练习的话,可以点这里。
文章目录
双指针核心思想:当数组或者字符串中的子数组或者子串具有一定的单调性的时候,可以使用双指针避免重复的计算。
两数之和ll - 输入有序数组
(二分)
因为数组是升序的,所以可以枚举nums[i]
,然后再O(1)的时间内找到target - nums[i]
。
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
while (l < r) {
int mid = l + (r - l + 1) / 2;
if (nums[mid] <= target) l = mid;
else r = mid - 1;
}
return l;
}
vector<int> twoSum(vector<int>& nums, int target) {
int len = nums.size();
for (int left = 0; left < len; left ++) {
int pos = search(nums, target - nums[left]);
if (nums[pos] + nums[left] == target) {
return {left + 1, pos + 1};
}
}
return {-1, -1};
}
};
(双指针)
因为数组是升序的,所以数组就具有了单调性,就可以使用双指针从数组的两端开始往里收缩,如果sum > target
因为nums[left]
已经是最小的数了,所以只能right --
,才可以使得两数之和减小。如果sum < target
,因为nums[right]
已经是最大的数了,所以只能left ++
,才可以使得两数之和增大。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int len = nums.size();
int left = 0, right = len - 1;
while (left < right) {
if (nums[left] + nums[right] < target) {
left ++;
} else if (nums[left] + nums[right] > target) {
right --;
} else {
return {left + 1, right + 1};
}
}
return {-1, -1};
}
};
三数之和
(暴力递归)TLE
使用回溯算法,可以将所有的不包含重复组合的三元组全部枚举出来,然后一个一个判断是否数组和等于0,如果等于0就将数组中的三元组放入ans中。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void dfs(vector<int>& nums, int sum, int start, int index) {
if (index == 3) {
if (sum == 0)
ans.push_back(path);
return;
}
for (int i = start; i < nums.size(); i ++) {
// 如果不是第一个并且相邻的两个数字相同,就直接跳过后面相同的数字
if (i != start && nums[i] == nums[i - 1]) continue;
path.push_back(nums[i]);
sum += nums[i];
dfs(nums, sum, i + 1, index + 1);
sum -= nums[i];
path.pop_back();
}
}
vector<vector<int>> threeSum(vector<int>& nums) {
// 排序数组,便于后面取出相同的组合
sort(nums.begin(), nums.end());
if (nums.size() < 3) return {};
dfs(nums, 0, 0, 0);
return ans;
}
};
(排序+双指针)
排序数组,使得数组具有单调性,然后使用双指针。
1.如果nums[i]>0
,则后面的排序数组后面的数字都大于0,所以不可能组合成0,直接跳出循环
2.使用if(i>0 && nums[i] == nums[i - 1])
直接跳过相邻且数值相同的后面的数字,这样是为了避免出现重复组合。
3.枚举nums[i]
是三元组中的一个数字,还需要枚举出其他两个数字,因为排序数组具有单调性,可以直接在[i + 1, nums.size() - 1]
的范围中找其他的两个数字。
3.1if (nums[i] + nums[left] + nums[right] < 0)
的话,因为数组已经排序了,所以nums[right]
已经是最大的数字了,所以这时想要使得三元组的和增大的话,只能让left++
.
3.2if(nums[i] + nums[left] + nums[right] > 0)
的话,因为数字已经排序了,所以nums[left]
已经是最下的数字了,所以这时想要使得三元组的和减小,只能让right--
。
3.3 如果nums[i] + nums[left] + nums[right] == 0
,就可以让三元组放入答案中了,但是如果nums[left+1]
和nums[left]
相等,或者nums[right-1]
和nums[right]
相等的话,又会包含重复的三元组了,所以需要跳过这些相同的元素。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int len = nums.size();
if (len < 3) return {};
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
for (int i = 0; i < len - 2; i ++) {
// 剪枝1,因为已经排序了,所以如果nums[i]>0
// 那么后面的数字都>0,三数之和就不能可等于0
if (nums[i] > 0) break;
// 剪枝2,如果相邻的两个数字都相同,则后面的数字直接跳过
if (i > 0 && nums[i] == nums[i - 1]) continue;
// 在[i+1, len-1]的范围内找两个数+nums[i]后可以等于0
// 在这个范围内找两个数字是因为这个范围内数字是有序的
int left = i + 1;
int right = len - 1;
while (left < right) {
// 利用数组的单调性来指定双指针范围
if (nums[left] + nums[right] + nums[i] < 0) {
left ++;
} else if (nums[left] + nums[right] + nums[i] > 0) {
right --;
} else {
ans.push_back({nums[left], nums[right], nums[i]});
// 为了避免重复的数字组合,使用while跳过相同的是数字
while (left < right && nums[left] == nums[left + 1]) left ++;
while (left < right && nums[right] == nums[right - 1]) right --;
left ++;
right --;
}
}
}
return ans;
}
};
最接近的三数之和
(排序+双指针)
这题和三数之和差不多,在三数之和那题中要求三元组的总和一定等于0,。而本题要求可以不等于target
,但必须要最接近target
,也就是说abs(sum - target)
最小才可以。
如果使用三层for
暴力循环求解所有的三元组,因为时间复杂度为O(N3),所以一定会超时。
所以可以使用先排序的方法使得数组变得具有单调性,然后就可以是使用双指针不断地逼近。
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
// 排序数组
sort(nums.begin(), nums.end());
// ans是接近target的三元组的和
int ans = nums[0] + nums[1] + nums[2];
int len = nums.size();
for (int i = 0; i < len - 2; i ++) {
int left = i + 1;
int right = len - 1;
while (left < right) {
int sum = nums[left] + nums[right] + nums[i];
if (abs(sum - target) < abs(ans - target)) {// 更新答案
ans = sum;
}
if (sum > target) {
right --;
} else if (sum < target) {
left ++;
} else {
return sum;// 剪枝,当sum==target的时候,一定是误差最小的时候
}
}
}
return ans;
}
};
接雨水
(暴力解法)TLE
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
int len = height.size();
// 枚举[1, height.size() - 2],分别算出两边最高的高度的最小值
for (int mid = 1; mid < len - 1; mid ++) {
// 左边柱子的最大值
int left = mid - 1, leftMax = 0;
while (left >= 0) {
leftMax = max(leftMax, height[left]);
left --;
}
// 右边柱子的最大值
int right = mid + 1, rightMax = 0;
while (right <= len - 1) {
rightMax = max(rightMax, height[right]);
right ++;
}
// 两个最大高度的最小值
int maxHeight = min(leftMax, rightMax);
// 如果高度大于当前位置的高度,就计算当前位置上的雨水
if (maxHeight > height[mid]) {
ans += maxHeight - height[mid];
}
}
return ans;
}
};
(以空间换时间优化-动态规划)
使用两个数组left_max[]
和right_max[]
来记录好第i
个位置左边和右边的最大值,这样就是节省了暴力方法中需要使用while
来统计的左右最大值的时间。
注意:left_max[i]
表示:第i
个位置左右的最大值,所以left_max[i + 1] = max(left_max[i], height[i])
;同理,right_max[i - 1] = max(right_max[i], height[i])
。
class Solution {
public:
int trap(vector<int>& height) {
int len = height.size();
// 存储每一个柱子左边的最大值
vector<int> leftMax(height.size());
for (int i = 1; i < len; i ++) {
leftMax[i] = max(leftMax[i - 1], height[i - 1]);// 这里要注意是height[i - 1]
}
// 存储每一个柱子右边的最大值
vector<int> rightMax(height.size());
for (int i = len - 2; i >= 0; i --) {
rightMax[i] = max(rightMax[i + 1], height[i + 1]);// 这里要注意是height[i+1]
}
int ans = 0;
for (int i = 1; i <= len - 2; i ++) {
int heightMax = min(leftMax[i], rightMax[i]);
if (heightMax > height[i]) {
ans += heightMax - height[i];
}
}
return ans;
}
};
(双指针)
由木桶原理可知,水量是由低短的一块木板决定的,所以如果左柱高右柱低,水量由右柱决定。如果右柱高左柱低,水量由左柱决定。
所以每一次只要比较最左边和最右边的柱子的高度,指针就可以在中间部分移动,而每一次的自身位置可以接多少雨水是由左边最大值(当左柱低的时候)或者右边最大值(当右柱低的时候)。如果当前的柱子高度比对应方向上柱子的最大值大的话,说明没有形成低洼地带,就可以更新最大值。如果当前柱子的高度比对应方向上的柱子最大值小的话,说明形成了低洼地带,可以接到maxVal - height[i]
的雨水。
这样来回切换遍历的方向,可以使得双指针只遍历一次数组就可以找到对应位置上的最大值。
class Solution {
public:
int trap(vector<int>& height) {
int len = height.size();
int left = 0, right = len - 1;
int left_max = 0, right_max = 0;
int ans = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] < left_max) {
ans += left_max - height[left];
} else {
left_max = height[left];
}
left ++;
} else {
if (height[right] < right_max) {
ans += right_max - height[right];
} else {
right_max = height[right];
}
right --;
}
}
return ans;
}
};
(单调栈)
只有低洼处才可以堆积雨水,所以只有当下一个柱子比当前柱子要高的时候才可以形成低洼处。
根据这一点可以想到使用单调栈。即维护一个单调递减的栈,因为每一次遇到高的柱子就会不进入栈中,而是和当前栈顶元素形成高度差,而在栈顶元素的位置上堆积雨水。但是是否堆积雨水不仅取决于右边的高柱子,而且需要有左边界,也就是说栈中的元素必须要>=2
才可以和新来的高柱子形成一个低洼处。
而且遇到一个高柱子有时可以触发多个柱子形成雨水
所以要使用while
一直累加上原来积累的雨水,而且每一次对应的雨水条的宽度和高度都是不一样的,因此每一次都要计算出柱子之间的高度差和宽度差。
class Solution {
public:
int trap(vector<int>& height) {
int len = height.size();
if (len < 3) return 0;
int ans = 0;
stack<int> sk;
int i = 0;
while (i < len) {
while (!sk.empty() && height[i] > height[sk.top()]) {// 如果可以形成低洼
int top = sk.top();
sk.pop();
if (sk.empty()) break;// 如果没有左边界了,就直接跳过
int w = i - sk.top() - 1;// 宽度
int h = min(height[i], height[sk.top()]) - height[top];// 高度
ans += h * w;
}
sk.push(i ++);
}
return ans;
}
};
盛最多水的容器
(暴力)TLE
暴力的逻辑就是让所有柱子之间全部都比较一遍,然后每次都计算盛水的量,最后得出最大值。
class Solution {
public:
int maxArea(vector<int>& height) {
int len = height.size();
int ans = 0;
for (int i = 0; i < len; i ++) {
for (int j = i + 1; j < len; j ++) {
ans = max(ans, (j - i) * min(height[i], height[j]));
}
}
return ans;
}
};
(双指针+贪心)
每一个柱子都要和其他柱子都比较一遍,这样就会产生很多的重复计算,并且这些结果对后续的比较没有产生有效可以利用的信息。
使用双指针算法就可以只比较一次相当于暴力算法遍历的一遍数组比较了所有的柱子,利用的就是贪心算法。首先让两个指针分别指向第一个柱子和最后一个柱子,因为这样性的雨水的矩形的长一定是最大的,此时移动较短的柱子向中间移动并计算雨水量,依次循环直到双指针相遇。
为什么每一次移动较短的柱子就可以排除一系列的可能性?根据木桶原理,水量是由最短的木板决定的,如果移动短的柱子,那么下一个柱子可能比现在最短的柱子要大也可能小,而且因为柱子向中间移动,所以水量矩形的长也会减小,但是还是有可能会变大的。但是如果移动较长的柱子的话,如果下一个柱子比当前的柱子要大,以为最短的柱子没有变,所以水量矩形的高度还是没有变化;如果下一个柱子比当前柱子要小,那么有可能比最短的柱子要小,还有可能比最短的柱子要大,比最短柱子要大水量还是没有变化,但是如果比最短柱子要小,而且因为向中间移动,所以水量矩形的长和宽都变小了,所以水量一定减少。
总结一下:就是如果移动较短的柱子,那么两个柱子的最短板可能增大可能减少,如果移动较短的柱子,那么两个柱子的最短板一定较少,所以每一次移动最短板就可以得到水量的最大值。
而在每一次移动最短板的时候就是排除了需要原来暴力算法需要遍历一遍数组的可能性,因为有柱子比当前柱子要大,那水量就不可能增大了(因为最短板已经定下来了),所以只要看到了一个大柱子,就不需要其他柱子再去比较了,应该立刻的使得最短板增大,而不是再看看有没有比自当前柱子还要低的或者是就算又高柱子水量也会因为当前柱子低而没有办法保存水量。
总之指针算法就是在一直的向上比较,发现当前位置一定没有办法再增加水量了就立刻地变化,就算水量不能提高,那也比当前停滞不前要好。
class Solution {
public:
int maxArea(vector<int>& height) {
int len = height.size();
int ans = 0;
int left = 0, right = len - 1;
while (left < right) {
if (height[left] < height[right]) {
ans = max(ans, (right - left) * height[left]);
left ++;
} else {
ans = max(ans, (right - left) * height[right]);
right --;
}
}
return ans;
}
};