4.6 接雨水【42】
4.6.1 题目描述
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
4.6.2 方法一:暴力
直观想法
直接按问题描述进行。对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。
算法
- 初始化 ans=0
- 从左向右扫描数组:
- 初始化 KaTeX parse error: Expected 'EOF', got '_' at position 10: \text{max_̲left}=0 和 max_right=0
- 从当前元素向左扫描并更新:
- KaTeX parse error: Expected 'EOF', got '_' at position 10: \text{max_̲left}=\max(\tex…
- 从当前元素向右扫描并更新:
- KaTeX parse error: Expected 'EOF', got '_' at position 10: \text{max_̲right}=\max(\te…
- 将KaTeX parse error: Expected 'EOF', got '_' at position 15: \min(\text{max_̲left},\text{max… 累加到 ans
public int trap(int[] height) {
int ans = 0;
int size = height.length;
for (int i = 1; i < size - 1; i++) { // 左右边界两条柱不可能蓄水,只能是中间条柱可以蓄水
int max_left = 0, max_right = 0;
for (int j = i; j >= 0; j--) { //查找左边最高的条形柱大小
max_left = Math.max(max_left, height[j]);
}
for (int j = i; j < size; j++) { //查找右边最高的条形柱大小
max_right = Math.max(max_right, height[j]);
}
ans += Math.min(max_left, max_right) - height[i];
}
return ans;
}
// 通过计算每一个柱形条上的蓄水量累加来得到答案
复杂性分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)。数组中的每个元素都需要向左向右扫描。
- 空间复杂度 O(1) 的额外空间。
4.6.3 方法二:动态规划
直观想法
在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态规划解决。
这个概念可以见下图解释:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9i9xJKlO-1657953094135)(.assets/image-20220715160235232.png)]
算法
- 找到数组中从下标 i 到最左端最高的条形块高度 left_max。
- 找到数组中从下标 i 到最右端最高的条形块高度 right_max。
- 扫描数组 height 并更新答案:
- 累加 KaTeX parse error: Expected 'EOF', got '_' at position 15: \min(\text{max_̲left}[i],\text{… 到 ans 上
public int trap(int[] height) {
if (height == null || height.length == 0)
return 0;
int ans = 0;
int size = height.length;
int[] left_max = new int[size]; // 用于将i位置左边最高柱形条值储存起来,不用每次都求值
int[] right_max = new int[size]; // 用于将i位置左边最高柱形条值储存起来,不用每次都求值
left_max[0] = height[0];
for (int i = 1; i < size; i++) {
left_max[i] = Math.max(height[i], left_max[i - 1]);
}
right_max[size - 1] = height[size - 1];
for (int i = size - 2; i >= 0; i--) {
right_max[i] = Math.max(height[i], right_max[i + 1]);
}
for (int i = 1; i < size - 1; i++) {
ans += Math.min(left_max[i], right_max[i]) - height[i];
}
return ans;
}
// 通过计算每一个柱形条上的蓄水量累加来得到答案
复杂性分析
- 时间复杂度:O(n)。
- 存储最大高度数组,需要两次遍历,每次 O(n) 。
- 最终使用存储的数据更新 ans ,O(n)。
- 空间复杂度:O(n) 额外空间。
- 和方法 1 相比使用了额外的 O(n) 空间用来放置 left_max 和 right_max 数组。
4.6.4 方法三:栈的应用
直观想法
我们可以不用像方法 2 那样存储最大高度,而是用栈来跟踪可能储水的最长的条形块。使用栈就可以在一次遍历内完成计算。
我们在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,我们将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果我们发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案到 ans 。
算法
- 使用栈来存储条形块的索引下标。
- 遍历数组:
- 当栈非空且 \text{height}[current]>\text{height}[st.top()]height[current]>height[st.top()]
- 意味着栈中元素可以被弹出。弹出栈顶元素 top。
- 计算当前元素和栈顶元素的距离,准备进行填充操作
distance = current − st.top ( ) − 1 \text{distance} = \text{current} - \text{st.top}() - 1 distance=current−st.top()−1 - 找出界定高度
KaTeX parse error: Expected 'EOF', got '_' at position 14: \text{bounded_̲height} = \min(… - 往答案中累加积水量 KaTeX parse error: Expected 'EOF', got '_' at position 61: …s \text{bounded_̲height}
- 将当前索引下标入栈
- 将 current 移动到下个位置
- 当栈非空且 \text{height}[current]>\text{height}[st.top()]height[current]>height[st.top()]
public int trap(int[] height) {
int ans = 0, current = 0;
Deque<Integer> stack = new LinkedList<Integer>();
while (current < height.length) {
while (!stack.isEmpty() && height[current] > height[stack.peek()]) {
int top = stack.pop();
if (stack.isEmpty())
break;
int distance = current - stack.peek() - 1;
int bounded_height = Math.min(height[current], height[stack.peek()]) - height[top];
ans += distance * bounded_height;
}
stack.push(current++);
}
return ans;
}
// 通过计算两个柱形条直接的蓄水量累加得到答案
复杂性分析
- 时间复杂度:O(n)。
- 单次遍历 O(n) ,每个条形块最多访问两次(由于栈的弹入和弹出),并且弹入和弹出栈都是 O(1) 的。
- 空间复杂度:O(n)。 栈最多在阶梯型或平坦型条形块结构中占用 O(n) 的空间。
4.6.5 方法四:使用双指针
直观想法
和方法 2 相比,我们不从左和从右分开计算,我们想办法一次完成遍历。
从动态规划方法的示意图中我们注意到,只要 right_max[i] > left_max[i] (元素 0 到元素 6),积水高度将由 left_max 决定,类似地 left_max[i]>right_max[i](元素 8 到元素 11)。
所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
我们必须在遍历时维护 left_max 和 right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。
算法(可参考LeetCode得动画,非常形象)
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int ans = 0;
int left_max = 0, right_max = 0;
while (left < right) {
if (height[left] < height[right]) { // 哪边的柱子低,蓄水量就依赖哪一边
if (height[left] >= left_max) {
left_max = height[left];
} else {
ans += (left_max - height[left]);
}
++left;
} else {
if (height[right] >= right_max) {
right_max = height[right];
} else {
ans += (right_max - height[right]);
}
--right;
}
}
return ans;
}
复杂性分析
- 时间复杂度:O(n)。单次遍历的时间O(n)。
- 空间复杂度:O(1) 的额外空间。left, right, left_max 和 right_max 只需要常数的空间。
4.6.6 my answer—栈
class Solution {
public int trap(int[] height) {
int ans = 0;
int cur = 0;
Deque<Integer> stack = new ArrayDeque<>();
while(cur < height.length){
while(!stack.isEmpty() && height[cur] > height[stack.peek()]){ // 栈非空,且当前值比栈顶值大
int top = stack.pop();
if(stack.isEmpty()){
break;
}
int left = stack.peek();
int curWith = cur - left -1;
//高度取两者较低的,还需减去中间已经计算过的高度
int curHeight = Math.min(height[cur],height[left]) - height[top];
ans += curWith*curHeight;
}
stack.push(cur++);
}
return ans;
}
}