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; }
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~