题目要求
Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.
For example,
Given [0,1,0,2,1,0,1,3,2,1,2,1], return 6.
假设这些是一些间隔的木板,问最多能够装多少水。也就是一个区域性的短板问题。其实一个区间能够乘的最大水量,取决于它的左右最近且最高的木板的长度。当然除了通过多个区间的和来计算总体的盛水量,还可以通过横向的划分来计算盛水量。这些将在接下来中的代码一一分析。官方也提供了一些答案,这里将给出相应的java实现的版本。
我的思路
这里先讲一讲我在拿到这个题目时候的思路。我先通过堆栈的方法,找到一个封闭区间,该区间可以盛水,该区间的右节点可以作为下一个封闭区间的起点。这种方法思路非常直接,但是并不是高效的计算机思维。
堆栈的方法如下:
//使用堆栈,分别存储值和下标
public int trap(int[] height) {
int length = height.length;
if(length<=2){
return 0;
}
Stack<Integer> s = new Stack<Integer>();
Stack<Integer> index = new Stack<Integer>();
s.push(0);
index.push(1);
int leftMost = 0;
int result = 0;
for(int i = 0 ; i<length ; i++){
int currentVal = height[i];
//如果当前值比最左值大,则说明形成了一个封闭的盛水区间
if(currentVal >= leftMost){
while(!s.isEmpty()){
result += (leftMost - s.pop()) * index.pop();
}
s.push(currentVal);
index.push(1);
leftMost = currentVal;
}else{
//如果当前值比最左值小,则说明该盛水区间仍然没到最右点
int count = 1;
//将所有比当前值小的区间填满,并将水平区间的个数插入栈中
while(currentVal > s.peek()){
count += index.peek();
result += (currentVal - s.pop()) * index.pop();
}
s.push(currentVal);
index.push(count);
}
}
return result;
}
使用双指针代替堆栈提高些许性能
//双指针 不使用堆栈
public int trap3(int[] height) {
int length = height.length;
if(length<=2){
return 0;
}
//获得可以盛水的区间
int startIndex = 0;
while(startIndex<length-1 && height[startIndex]<=height[startIndex+1]){
startIndex++;
}
int result = 0;
//这里思路类似于上一段代码,只是可以填水的区间将值改为填水后的值
int index = startIndex;
while(++index < length){
int currentHeight = height[index];
if(currentHeight > height[startIndex]){
for(int i = index-1 ; i > startIndex ; i--){
result += (height[startIndex] - height[i]);
}
startIndex = index;
}else{
for(int i = index ; i>0 && height[i] > height[i-1] ; i--){
result += (height[i] - height[i-1]);
height[i-1] = height[i];
}
}
}
return result;
}
这里会降低代码效率的部分主要在于,对于盛水区间的高度的计算太冗杂了。只要获得左右木板高度的最小值,就是当前区间可以盛水的最大高度。在这里最大的问题就在于获得最远区间后,需要对区间内的小区间逐个遍历。这种方法虽然一次遍历数组就可以实现,但是还需要在子区间中反复计算才可以。
思路一:强行遍历
从整个数组的角度看来,如果找到区间左侧的最大值leftMax,以及区间右侧的最大值rightMax,就可以知道当前区间的盛水高度为Math.min(leftMax,rightMax)-height[i]。也就是传说中的木桶理论,短板决定盛水的高度。这样的话,代表在每个区间计算盛水值的时候,需要遍历整个数组。遍历整个数组,则需要O(n的平方)的时间复杂度。
这里的代码实现并不难,可以直接参考官方提供的C++方法。
思路二:动态编程 先遍历获得区间左右最大值再计算
思路一中,每一次对一个区间都要遍历整个数组才能获得左右最大值。但是其实,从左往右遍历一次数组,可以获得各个区间的leftMax
。同理,从右往左遍历可以获得各个区间的rightMax
。将这两个值都存在数组中,并对数组进行遍历,计算各个区间的盛水高度。时间复杂度为O(n),代码如下:
public int trap3(int[] height){
int length = height.length;
//leftMax数组
int[] left = new int[length];
//rightMax数组
int[] right = new int[length];
int leftMax = 0;
int rightMax = 0;
for(int i = 0 ; i<length ; i++){
leftMax = left[i] = Math.max(leftMax, height[i]);
rightMax = right[length-i-1] = Math.max(rightMax, height[length-i-1]);
}
int result = 0;
for(int j = 0 ; j<length ; j++){
result += Math.min(left[j], right[j]) - height[j];
}
return result;
}
该思路的官方讲解请戳这里
这里涉及了一个解题思路,叫做Dynamic Programming。这在我之前的博客中也有所提及,但是我仍然对这个概念比较模糊。这里有一个非常好的解答帖子供大家参考。笼统的来说,就是利用已知的解答来帮助解决目标问题。看上去好像是一句废话,但是具体实践中有多重形式,例如递归,自顶向下编码和自底向上编码等等。这些概念还是需要通过大量的题目和代码来感受啊。
思路三:堆栈的聪明使用
在这里,堆栈允许我们渐进的通过横向分割而非之前传统的纵向分割的方式来累加计算盛水量。一旦当前区间的高度超过栈顶的元素,就代表栈顶元素有一个右边界。鉴于栈中的元素都是递减的,所以如果存在一个比栈顶元素大的栈中元素,则一定可以确定该区间的盛水量。代码如下:
public int trap4(int[] height){
int length = height.length;
int result = 0, current = 0;
Stack<Integer> s = new Stack<Integer>();
while(current < length){
while(!s.isEmpty() && height[current] > height[s.peek()]){
int top = s.pop();
if(s.isEmpty()){
break;
}
//获得两个节点之间的宽度
int distance = current - s.peek() - 1;
int tempHeight = Math.min(height[current], height[s.peek()]) - height[top];
result += tempHeight * distance;
}
s.push(current++);
}
return result;
}
思路四:双指针的进阶
这里要从更高的高度来看这道题目,原文如下:
这里的意思是这样的:
经过实践证明,如果当前区间的leftMax<rightMax,则当前区间的盛水量由leftMax决定,反之则由rightMax决定。
换句话说,假设能找到任意一个比当前区间高度值大的值,并且假设该值位于当前区间的右侧,那么该区间的盛水高度由leftMax决定。同理,如果该值位于当前区间的左侧,那么该区间的盛水高度由rightMax决定。
这个结论可以使用反证法证明。
假设能找到任意一个比当前区间高度值大的值,并且假设该值位于当前区间的右侧,则存在满足这样一个区间,该区间盛水高度由rightMax决定。
这说明,该区间的leftMax大于rightMax。假设当前区间下标为i,则一定存在一个j<=i,height[j]=leftMax。其实,纵观双指针的遍历,我们可以知道,双指针最终会停在高度最高的区间,即height[index] = Max(leftMax, rightMax)上。每一次遍历都会将两个指针向相对较高的位置移动。
所以一旦左指针遍历到刚才的j节点,因为当前右指针指向的值都小于leftMax(rightMax<leftMax),右指针则会一直移动,不可能出现当前的情况。
代码实现如下:
public int trap5(int[] height) {
int left = 0;
int right = height.length - 1;
int result = 0;
int leftMax=0, rightMax=0;
while(left < right){
if(height[left] < height[right]){
leftMax = Math.max(height[left], leftMax);
result += leftMax - height[left];
left++;
}else{
rightMax = Math.max(height[right], rightMax);
result += rightMax - height[right];
right--;
}
}
return result;
}
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。