文章目录
- 写在前面
- Tag
- 题目来源
- 解题思路
- 方法一:预处理
- 方法二:单调栈
- 方法三:双指针
- 写在最后
写在前面
本专栏专注于分析与讲解【面试经典150】算法,两到三天更新一篇文章,欢迎催更……
专栏内容以分析题目为主,并附带一些对于本题涉及到的数据结构等内容进行回顾与总结,文章结构大致如下,部分内容会有增删:
- Tag:介绍本题牵涉到的知识点、数据结构;
- 题目来源:贴上题目的链接,方便大家查找题目并完成练习;
- 题目解读:复述题目(确保自己真的理解题目意思),并强调一些题目重点信息;
- 解题思路:介绍一些解题思路,每种解题思路包括思路讲解、实现代码以及复杂度分析;
- 知识回忆:针对今天介绍的题目中的重点内容、数据结构进行回顾总结。
Tag
【数组】【单调栈】【双指针】
题目来源
42. 接雨水
解题思路
方法一:预处理
思路
一个朴素的做法是对于数组 height
中的每个元素,分别向左和向右扫描并记录左边的最大高度,然后计算每个下标位置能接到的雨水量。
假设数组 height
的长度为
n
n
n,朴素的做法需要对每个下标位置使用
O
(
n
)
O(n)
O(n) 的时间向两边扫描并获得最大高度,总的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
本题数据规模达到 1 0 5 10^5 105,时间复杂度为 O ( n 2 ) O(n^2) O(n2) 的解法一定超时。
预处理
上述做法的时间复杂度较高是因为需要对每个下标位置都向两边扫描。如果已经知道每个位置两边的最大高度,则可以在 O(n)O(n)O(n) 的时间内得到能接的雨水总量。
于是可以提前计算并记录每个位置两侧的最大高度。
具体地,维护数组 leftMax
和 rightMax
,其中 leftMax[i]
表示下标 i
及其左边的位置中,height
的最大高度。rightMax[i]
表示下标 i
及其右边的位置中,height
的最大高度。显然有初始化 leftMax[0] = height[0]
,rightMax[n-1] = height[n-1]
,其余位置的元素计算比较简单,直接看代码。
代码
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if (n == 0) {
return 0;
}
// 更新 leftMax
vector<int> leftMax(n);
leftMax[0] = height[0];
for (int i = 1; i < n; ++i) {
leftMax[i] = max(leftMax[i-1], height[i]);
}
// 更新 rightMax
vector<int> rightMax(n);
rightMax[n-1] = height[n-1];
for (int i = n - 2; i >= 0; --i) {
rightMax[i] = max(rightMax[i+1], height[i]);
}
int res = 0;
for (int i = 0; i < n; ++i) {
res += min(leftMax[i], rightMax[i]) - height[i];
}
return res;
}
};
复杂度分析
时间复杂度: O ( n ) O(n) O(n)。
空间复杂度: O ( n ) O(n) O(n)。
方法二:单调栈
思路
除了计算并存储每个位置两边的最大高度以外,也可以用单调栈计算能接的雨水总量。
维护一个单调栈,单调栈存储的是下标,满足从栈底到栈顶的下标对应的数组 height
中的元素递减。
从左到右遍历数组,遍历到下标 i
时,如果栈内至少有两个元素,记栈顶元素为 top
,top
的下面一个元素是 left
,则一定有 height[left]≥height[top]
。如果 height[i]>height[top]
,则得到一个可以接雨水的区域,该区域的宽度是 i−left−1
,高度是 min(height[left],height[i])−height[top]
,根据宽度和高度即可计算得到该区域能接的雨水量。
为了得到 left
,需要将 top
出栈。在对 top
计算能接的雨水量之后,left
变成新的 top
,重复上述操作,直到栈变为空,或者栈顶下标对应的 height
中的元素大于或等于 height[i]
。
在对下标 i
处计算能接的雨水量之后,将 i
入栈,继续遍历后面的下标,计算能接的雨水量。遍历结束之后即可得到能接的雨水总量。
代码
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size(), res = 0;
stack<int> stk;
for (int i = 0; i < n; ++i) {
while (!stk.empty() && height[i] > height[stk.top()]) {
int top = stk.top(); stk.pop();
if (stk.empty()) {
break;
}
int left = stk.top();
int curWidth = i - left - 1;
int curHeigh = min(height[left], height[i]) - height[top];
res += curWidth * curHeigh;
}
stk.push(i);
}
return res;
}
};
复杂度分析
时间复杂度:
O
(
n
)
O(n)
O(n),
n
n
n 是数组 height
的长度。从 0 到 n-1 的每个下标最多只会入栈和出栈一次。
空间复杂度: O ( n ) O(n) O(n)。空间复杂度主要取决于栈空间,栈的大小不会超过 n。
方法三:双指针
思路
下标 i
处能接的雨水量由 leftMax[i]
和 rightMax[i]
中的最小值决定。数组 leftMax
是从左到右计算的,数组 rightMax
是从右到左计算的,我们可以使用双指针和两个变量来代替两个数组。
使用 height[left]
和 height[right]
来更新 leftMax
和 rightMax
比较容易理解就不说了。重点说一下如何更新 res
:
- 当
height[left] < heigh[right]
时,必有leftMax < rightMax
,下标left
处一定可以盛水; - 当
height[left] >= heigh[right]
时,必有leftMax >= rightMax
,下标right
处一定可以盛水。
代码
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
while (left < right) {
leftMax = max(leftMax, height[left]);
rightMax = max(rightMax, height[right]);
if (height[left] < height[right]) {
res += leftMax - height[left++];
}
else {
res += rightMax - height[right--];
}
}
return res;
}
};
复杂度分析
时间复杂度:
O
(
n
)
O(n)
O(n),
n
n
n 为数组 height
的长度。
空间复杂度: O ( 1 ) O(1) O(1)。
写在最后
如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。
如果大家有更优的时间、空间复杂度的方法,欢迎评论区交流。
最后,感谢您的阅读,如果有所收获的话可以给我点一个 👍 哦。