建议全文背诵
题目
https://leetcode-cn.com/problems/trapping-rain-water/
方法一:暴力
分析
对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。
复杂度
时间复杂度:O(n²)
空间复杂度:O(1)
代码
class Solution {
public int trap(int[] height) {
if(height==null)
return 0;
int len=height.length;
if(len==0)
return 0;
int left=0,right=0;
int res=0;
for(int i=1;i<len-1;i++){
left=0;
right=0;
for(int j=i;j>=0;j--){
left=Math.max(left,height[j]);
}
for(int j=i;j<len;j++){
right=Math.max(right,height[j]);
}
res+=Math.min(left,right)-height[i];
}
return res;
}
}
结果
方法二:动态规划
分析
在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态编程解决。
找到数组中从下标 i 到最左端最高的条形块高度 left_max。
找到数组中从下标 i 到最右端最高的条形块高度 right_max。
扫描数组 height 并更新答案:
累加res+=Math.min(left_max[i],right_max[i])-height[i],得出结果。
复杂度
时间复杂度:O(n)。
存储最大高度数组,需要两次遍历,每次 O(n) 。
最终使用存储的数据更新ans ,O(n)。
空间复杂度:O(n) 额外空间。
和方法 1 相比使用了额外的 O(n)空间用来放置left_max 和 right_max 数组。
代码
class Solution {
public int trap(int[] height) {
if(height==null)
return 0;
int len=height.length;
if(len==0)
return 0;
int res=0;
int[] left_max=new int[len];
int[] right_max=new int[len];
left_max[0]=height[0];
//找到i左边的最大值
for(int i=1;i<len;i++){
left_max[i]=Math.max(height[i],left_max[i-1]);
}
right_max[len-1]=height[len-1];
//找到i右边的最大值
for(int i=len-2;i>=0;i--){
right_max[i]=Math.max(height[i],right_max[i+1]);
}
//计算结果
for(int i=1;i<len-1;i++){
res+=Math.min(left_max[i],right_max[i])-height[i];
}
return res;
}
}
结果
方法三:栈
分析
跟动态规划/双指针的不同是,这里是按行求,而非按列求。
栈内x、栈顶y、当前z,有height[z]>height[y],height[x]>height[y],那么长度为z-x-1,高度为min(height[x],height[z])-height[y]。
维护一个单调递减栈。
我们在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,我们将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果我们发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案 。
使用栈来存储条形块的索引下标。 遍历数组:
1.当栈非空且 height[current]>height[st.top()] 意味着栈中元素可以被弹出。弹出栈顶元素 top。 计算当前元素和栈顶元素的距离,准备进行填充操作 distance=current−st.top()−1 找出界定高度
bounded_height=min(height[current],height[st.top()])−height[top]
往答案中累加积水量 res+=distance×bounded_height
2.将当前索引下标入栈
3.将 current 移动到下个位置
复杂度
时间复杂度:O(n)。
单次遍历 O(n) ,每个条形块最多访问两次(由于栈的弹入和弹出),并且弹入和弹出栈都是 O(1) 的。
空间复杂度:O(n)。 栈最多在阶梯型或平坦型条形块结构中占用 O(n) 的空间。
代码
class Solution {
public int trap(int[] height) {
if(height==null||height.length==0)
return 0;
int len=height.length;
Deque<Integer> stack=new LinkedList<>();
int current=0;
int res=0;
while(current<len){
while(!stack.isEmpty()&&height[stack.peekLast()]<height[current]){
int top=stack.removeLast();
if(stack.isEmpty())
break;
int before=stack.peekLast();//top在栈中的前一个
int width=current-before-1;//before和current之间的就是可以接的,宽度
int h=Math.min(height[before],height[current])-height[top];//高度
res+=width*h;
}
//循环结束时,栈里没有大于当前数的了,将当前数入栈满足栈顶最小。
stack.addLast(current);
current++;//遍历下一个数
}
return res;
}
}
结果
方法四:双指针
分析
所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
我们必须在遍历时维护left_max 和right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。
复杂度
时间复杂度:O(n)。单次遍历的时间O(n)。
空间复杂度:O(1) 的额外空间。left,right, left_max 和 right_max 只需要常数的空间。
代码
class Solution {
public int trap(int[] height) {
int n=height.length;
if(n<3) return 0;//不能装水
int left_max=0;//左边最高下标
int right_max=n-1;//右边最高下标
int left=0;//左边
int right=n-1;//右边
int sum=0;
while(left<right){
//height[left]<height[right]<=height[right_max],取决于左边
if(height[left]<height[right]){
if(height[left_max]>height[left]){//左边最大值大于left
sum+=height[left_max]-height[left];
}
else{//左边最大值小于等于left,不能装水
left_max=left;
}
left++;
}
else{
if(height[right_max]>height[right]){
sum+=height[right_max]-height[right];
}
else{
right_max=right;
}
right--;
}
}
return sum;
}
}
class Solution {
public int trap(int[] height) {
if(height==null)
return 0;
int len=height.length;
if(len==0)
return 0;
int res=0;
int left=0;
int right=len-1;
int left_max=0;
int right_max=len-1;
while(left<right){
if(height[left]<height[right]){
if(height[left]>height[left_max])
left_max=left;
else
res+=height[left_max]-height[left];
left++;
}
else{
if(height[right]>height[right_max])
right_max=right;
else
res+=height[right_max]-height[right];
right--;
}
}
return res;
}
}