接雨水
分析:
height = [0,1,0,2,1,0,1,3,2,1,2,1]
观察图片可以发现每一列能接多少水取决于:当前格子左边最高的和右边最高的其中较低的和当前格子的差值
例如:
第3个格子左边最大是第2个格子,右边最大是第8个格子,受限于左边的格子,所以只能存一格水
第5个格子左边最大是第4个格子,右边最大是第8个格子,受限于第4个格子,最高只能到2,但是自身只有1的高度,所以只能存1个格子的水
解法一 暴力:
第一层循环从1 - n-2,第二层循序遍历找左边最大的和右边最大的;找到每一列能存多少水,然后存起来:
class Solution {
public int trap(int[] height) {
int res = 0;
for (int i = 1; i < height.length - 1; i++) {
int lMax = 0;
for (int j = 0; j < i; j++) {
lMax = Math.max(lMax, height[j]);
}
int rMax = 0;
for (int j = i + 1; j < height.length; j++) {
rMax = Math.max(rMax, height[j]);
}
if (lMax > height[i] && rMax > height[i]) {
res += Math.min(lMax, rMax) - height[i];
}
}
return res;
}
}
时间复杂度:o(n*n)
空间复杂度:o(1)
解法二 动态规划:
解法一中每查一个格子都需要遍历这个格子左边所有和右边所有的,有很多重复的查询,因此考虑采用动态规划,用空间换时间的方式,减少时间复杂度:
- 递推公式含义:n位置左边的最大值和右边的最大值
- 递推公式:Fn = max(Fn-1, height[i-1]);
- dp数组初始化:为 0 即可,因为0位置左边最大值是0,n-1位置右边最大值为0
- 遍历顺序:遍历两次,一次从左到右(1到n-2),一次从右到左(n-2 到 1)
- 举例:F1 = max(height[0], F0) = max(0, 0);
class Solution {
public static int trap(int[]height) {
int n = height.length;
int[]leftMax = new int[n];
int[]rightMax = new int[n];
for (int i = 1; i < n - 1; i++) {
leftMax[i] =Math.max(leftMax[i - 1], height[i - 1]);
} for (int i = n - 2; i > 0; i--) {
rightMax[i] =Math.max(rightMax[i + 1], height[i + 1]);
} int sum = 0;
for (int i = 1; i < n - 1; i++) {
int v = Math.min(leftMax[i], rightMax[i])-height[i];
sum += Math.max(v, 0);
} return sum;
}
}
时间复杂度:o(n)
空间复杂度:o(n)
解法三 双指针:
一个格子能存多少水取决于它左右矮的一方。
上面动态规划的方法需要使用n的空间保持左边和右边的最大值,但是实际上数组中的每一个格子都只使用了一次,并且格子能存多少水实际是取决于左右最高比较矮的一个,那么就可以使用双指针的方式,一个指针从0开始,一个从n-1开始,定义lmax为左边的最大值,rmax 为右边最大值,移动规则是高的不动,低的移动:
lmax > rmax 的时候说明左边比右边高,可以得知j左边的最大值一定是大于j右边的最大值的,这时候我们可以j这个格子能存多少水,然后j左移一位
lmax < rmax 的时候说明左边比右边矮,可以得知i右边的最大值一定是大于i左边的最大值的,这时候我们可以i这个格子能存多少水,然后i右移一位
这个时候我们发现lmax和rmax是相等的,由此可以推断一个结论:i的右边的最大值肯定是大于等于i左边的;或者j左边的最大值一定是大于等于i右边的,因此可以将i能存的水位加上,i右移一位,或者加上j的水位,j左移一位
以此类推
代码:
class Solution {
public static int trap(int[]height) {
int n = height.length;
int i = 0, j = n - 1;
int lmax = 0, rmax = 0;
int sum = 0;
while (i < j) {
lmax = Math.max(lmax, height[i]);
rmax = Math.max(rmax, height[j]);
if (lmax >= rmax) {
sum += Math.max(0, rmax - height[j]);
j--;
} else {
sum += Math.max(0, lmax - height[i]);
i++;
}
} return sum;
}
}
解法四 单调栈:
前面的三种解法我们都是通过求每一列能存多少水,那么是否可以求每一行能存多少水呢?
观察规律可以发现:当后面的柱子比当前柱子高的时候,就可以求一行的积水,例如:
这时候能存多少水,就取决于长*宽
这时候我们就可以采用一个栈的数据结构,当栈顶元素比当前下标元素高的时候,入栈,否则取出栈顶元素,那栈顶元素的前一个下标和当前下标的距离表示框,高就是栈顶元素与当前元素的高度差
代码:
class Solution {
public int trap(int[]height) {
if (null == height || 0 == height.length) {
return 0;
}
Deque<Integer> deque = new ArrayDeque<>();
int sum = 0;
int i = 0;
while (i < height.length) {
if (deque.isEmpty()) {
deque.addLast(i);
} else if (height[deque.peekLast()] >height[i]){
deque.addLast(i);
} else if (height[deque.peekLast()] <height[i]){
int mid = deque.pollLast();
if (!deque.isEmpty()) {
sum += (Math.min(height[deque.peekLast()], height[i])-height[mid]) *(i - deque.peekLast() - 1);
continue;
} else {
deque.addLast(i);
}
} else{
deque.pollLast();
deque.add(i);
}
i++;
} return sum;
}
}