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实现的版本。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),代码以下:app
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; }
这里要从更高的高度来看这道题目,原文以下:spa
这里的意思是这样的:指针
通过实践证实,若是当前区间的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; }
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注个人微信公众号!将会不按期的发放福利哦~