讀完本文,你能夠去力扣拿下以下題目:java
42.接雨水python
-----------git
接雨水這道題目挺有意思,在面試題中出現頻率還挺高的,本文就來步步優化,講解一下這道題。github
先看一下題目:面試
就是用一個數組表示一個條形圖,問你這個條形圖最多能接多少水。算法
int trap(int[] height);
下面就來由淺入深介紹暴力解法 -> 備忘錄解法 -> 雙指針解法,在 O(N) 時間 O(1) 空間內解決這個問題。數組
我第一次看到這個問題,機關用盡,徹底沒有思路,相信不少朋友跟我同樣。因此對於這種問題,咱們不要想總體,而應該去想局部;就像以前的文章處理字符串問題,不要考慮如何處理整個字符串,而是去思考應該如何處理每個字符。緩存
這麼一想,能夠發現這道題的思路其實很簡單。具體來講,僅僅對於位置 i,能裝下多少水呢?app
能裝 2 格水。爲何剛好是兩格水呢?由於 height[i] 的高度爲 0,而這裏最多能盛 2 格水,2-0=2。優化
爲何位置 i 最多能盛 2 格水呢?由於,位置 i 能達到的水柱高度和其左邊的最高柱子、右邊的最高柱子有關,咱們分別稱這兩個柱子高度爲 l_max
和 r_max
;位置 i 最大的水柱高度就是 min(l_max, r_max)
。
更進一步,對於位置 i,可以裝的水爲:
water[i] = min( # 左邊最高的柱子 max(height[0..i]), # 右邊最高的柱子 max(height[i..end]) ) - height[i]
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
這就是本問題的核心思路,咱們能夠簡單寫一個暴力算法:
int trap(vector<int>& height) { int n = height.size(); int ans = 0; for (int i = 1; i < n - 1; i++) { int l_max = 0, r_max = 0; // 找右邊最高的柱子 for (int j = i; j < n; j++) r_max = max(r_max, height[j]); // 找左邊最高的柱子 for (int j = i; j >= 0; j--) l_max = max(l_max, height[j]); // 若是本身就是最高的話, // l_max == r_max == height[i] ans += min(l_max, r_max) - height[i]; } return ans; }
有以前的思路,這個解法應該是很直接粗暴的,時間複雜度 O(N^2),空間複雜度 O(1)。可是很明顯這種計算 r_max
和 l_max
的方式很是笨拙,通常的優化方法就是備忘錄。
以前的暴力解法,不是在每一個位置 i 都要計算 r_max
和 l_max
嗎?咱們直接把結果都緩存下來,別傻不拉幾的每次都遍歷,這時間複雜度不就降下來了嘛。
咱們開兩個數組 r_max
和 l_max
充當備忘錄,l_max[i]
表示位置 i 左邊最高的柱子高度,r_max[i]
表示位置 i 右邊最高的柱子高度。預先把這兩個數組計算好,避免重複計算:
int trap(vector<int>& height) { if (height.empty()) return 0; int n = height.size(); int ans = 0; // 數組充當備忘錄 vector<int> l_max(n), r_max(n); // 初始化 base case l_max[0] = height[0]; r_max[n - 1] = height[n - 1]; // 從左向右計算 l_max for (int i = 1; i < n; i++) l_max[i] = max(height[i], l_max[i - 1]); // 從右向左計算 r_max for (int i = n - 2; i >= 0; i--) r_max[i] = max(height[i], r_max[i + 1]); // 計算答案 for (int i = 1; i < n - 1; i++) ans += min(l_max[i], r_max[i]) - height[i]; return ans; }
這個優化其實和暴力解法差很少,就是避免了重複計算,把時間複雜度下降爲 O(N),已是最優了,可是空間複雜度是 O(N)。下面來看一個精妙一些的解法,可以把空間複雜度下降到 O(1)。
這種解法的思路是徹底相同的,但在實現手法上很是巧妙,咱們此次也不要用備忘錄提早計算了,而是用雙指針邊走邊算,節省下空間複雜度。
首先,看一部分代碼:
int trap(vector<int>& height) { int n = height.size(); int left = 0, right = n - 1; int l_max = height[0]; int r_max = height[n - 1]; while (left <= right) { l_max = max(l_max, height[left]); r_max = max(r_max, height[right]); left++; right--; } }
對於這部分代碼,請問 l_max
和 r_max
分別表示什麼意義呢?
很容易理解,l_max
是 height[0..left]
中最高柱子的高度,r_max
是 height[right..end]
的最高柱子的高度。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。
明白了這一點,直接看解法:
int trap(vector<int>& height) { if (height.empty()) return 0; int n = height.size(); int left = 0, right = n - 1; int ans = 0; int l_max = height[0]; int r_max = height[n - 1]; while (left <= right) { l_max = max(l_max, height[left]); r_max = max(r_max, height[right]); // ans += min(l_max, r_max) - height[i] if (l_max < r_max) { ans += l_max - height[left]; left++; } else { ans += r_max - height[right]; right--; } } return ans; }
你看,其中的核心思想和以前如出一轍,換湯不換藥。可是細心的讀者可能會發現次解法仍是有點細節差別:
以前的備忘錄解法,l_max[i]
和 r_max[i]
表明的是 height[0..i]
和 height[i..end]
的最高柱子高度。
ans += min(l_max[i], r_max[i]) - height[i];
可是雙指針解法中,l_max
和 r_max
表明的是 height[0..left]
和 height[right..end]
的最高柱子高度。好比這段代碼:
if (l_max < r_max) { ans += l_max - height[left]; left++; }
此時的 l_max
是 left
指針左邊的最高柱子,可是 r_max
並不必定是 left
指針右邊最高的柱子,這真的能夠獲得正確答案嗎?
其實這個問題要這麼思考,咱們只在意 min(l_max, r_max)
。對於上圖的狀況,咱們已經知道 l_max < r_max
了,至於這個 r_max
是否是右邊最大的,不重要,重要的是 height[i]
可以裝的水只和 l_max
有關。
_____________
個人 在線電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 算法倉庫 已經得到了 70k star,歡迎標星!