1. 程式人生 > >面試官在“逗”你係列:到底應該怎麼爬樓梯?!

面試官在“逗”你係列:到底應該怎麼爬樓梯?!

## 直奔主題 演算法題是在面試過程中考察候選人邏輯思維能力、手寫程式碼能力的一種方式,因為有一句古話說的好:“說一千道一萬,不如寫段程式碼看一看”。 ![對,就是我說的](https://img2020.cnblogs.com/other/1103694/202102/1103694-20210210113120560-1174718087.jpg) 今天我們就來個單刀直入,直奔主題,從一個真實面試題```到底怎麼爬樓梯```來聊一聊演算法中的```動態規劃``` 。 ## 面試真題 小明家有一樓梯共有10級臺階,每次可以爬1級或2級,問小明爬到第10級臺階,一共有多少種走法? > 為什麼是“小明”呢?這是個奇怪的問題~ ## 真題分析 很多同學在第一次遇到這個爬樓梯的問題可能會比較```懵```,不知道該如何來解決。我們首先需要做的就是尋找這個問題的關鍵點:```每次只能爬1級或2級```。 ## 遞迴思想 小明每次只能爬1級或2級,那麼對於爬到第10級臺階來說,最後一次操作為走1級(此時處於第9級臺階上)或走2級(此時處於第8級臺階上)。 假定我們有個表示式f可以來計算到達某階臺階的走法,那麼對於第10階來說,這個表示式就應該為:``` f(10) = f(9) + f(8) ```。 > 對於這個表示式,是不是有種瞬間回到那初、高中的年代~ 按如上規則,再次考慮,爬到第9級臺階時,最後一次操作為走1級(此時處於第8級臺階上)或走2級(此時處於第7級臺階上),此處的表示式為:``` f(9) = f(8) + f(7) ```。 ...... 依次處理,當爬到第3級臺階時,計算的表示式就是``` f(3) = f(2) + f(1) ```。 那爬到第2級臺階有幾種方式呢:每次走1級或者一次走2級,也就是一共有2種走法,``` f(2) = 2 ```。 爬到第1級臺階的方式肯定只有一種:走1級,``` f(1) = 1 ```。 按我們的思考邏輯,相關程式碼如下: ```javascript /** * @method climbStairs * @description 爬樓梯 * @param {number} n 樓梯臺階數 * @return {number} 一共有多少種走法 */ function climbStairs (n) { if (n === 1) { return 1 }; if (n === 2) { return 2 }; let num = 0; num = climbStairs(n - 1) + climbStairs(n - 2); return num; } // 呼叫函式,輸出結果 let num = climbStairs(10); console.log(num); // 89 ``` > Congratulations~我們已經完成啦,得到了正確的結果。 就在你滿臉微笑、志得意滿的向面試官講解思路的時候,面試官十有八九會一副老狐狸得逞的樣子,繼續問你,假如n的值比較大的,如40之類的,上面定義的```climbStairs```的執行效能如何。 我們來看下執行的效能: 測試程式碼如下: ```javascript console.time(); let num = climbStairs(40); console.log(num); console.timeEnd() ``` 我的mac配置如下: ``` MacBook Pro 處理器:2.5 GHz 四核Intel Core i7 記憶體: 16GB ``` 連續執行三次資料如下: |序號|結果|執行時間| |----|----|----| |1| 165580141 | 811.077ms | |2| 165580141 | 817.025ms | |3| 165580141 | 814.803ms | > 注:在執行過程中有卡頓,不是瞬間輸出~,如果執行的是```climbStairs(100)```,你應該會瞬間聽到風扇啟動的嗚嗚聲~ ## 遞迴思想優化 在上面程式碼``` climbStairs ```的基礎上我們來進行優化處理。我們仔細分析程式碼的執行流程,就會發現有很多重複計算的地方,比如說```f(5)```就會在```f(6-1)```、```f(7-2)```時被重複計算,這就浪費了時間和效能。 那我們就選擇使用空間換時間的策略,設定物件```numbers```,儲存爬到某級臺階的結果,避免重複計算,```numbers```物件的結果如下: ``` let numbers = { 1: 1, 2: 2 } ``` 優化後代碼如下: ```javascript /** * @method climbStairs * @description 爬樓梯 * @param {number} n 樓梯臺階數 * @return {number} 一共有多少種走法 */ function climbStairs (n) { // 儲存計算的結果,key(臺階) : num(走法) let numbers = { 1: 1, 2: 2 }; let tmpClimbStairs = function (n) { // 已存在的資料,直接返回,不再重新計算 if (numbers[n]) { return numbers[n]; } // 不存在的資料,進行計算 let num = tmpClimbStairs(n - 1) + tmpClimbStairs(n - 2); // 計算完成後,存放如numbers中,下次可以直接使用 numbers[n] = num; // 返回結果 return num; } // 計算結果 let num = tmpClimbStairs(n); // 返回結果 return num; } ``` 相同環境下,我們再來執行測試,連續執行三次資料如下: |序號|結果|執行時間| |----|----|----| |1| 165580141 | 7.100ms | |2| 165580141 | 7.478ms | |3| 165580141 | 6.260ms | > 消耗的時間竟然相差百倍之多,It's amazing!說明我們使用空間換時間的策略是正確的。 執行結果幾乎是瞬間輸出的,執行如絲襪奶茶般順滑~此時此刻你可以再次執行``` climbStairs(100) ```來體驗下絕對的效能飆升! 這道面試題處理成這樣已經是非常OK的了,但是如果你已經感到徹底滿足,為自己的聰明才智感到驕傲了,你就會聽到面試官可愛(恨)的聲音傳來:”還有別的方法或效能更好的方法來實現嗎?“ 是不是心中一口老血想噴出來~面試官是不是故意的,是不是在針對我~ 哈哈,不慌不慌,小場面~ ## 遞迴與遞推 遞迴與遞推是兩種不同的看待、分析問題的思路。 ```遞迴```:自頂向下的處理邏輯,有相應的臨界點(終止遞迴的點); ```遞推```:自底向上的處理邏輯,到達目標點結束。 ### 遞推思想 我們重新使用```遞推```的方式再來看這個問題。 1. 爬到第1級臺階,有1種方式。 ```f(1) = 1```; 2. 爬到第2級臺階,有2種方式:每次1級或1次2級。```f(2) = 2```; 3. 爬到第3級臺階的情況呢? 不要忘了我們之前分析的關鍵點:每次只能1級或2級, 對於第3級臺階來說,可以是從第1級臺階出發也可以是從第2級臺階出發, 所以```f(3) = f(2) + f(1)``` 4. 同理可得爬到第4級臺階的情況,```f(4) = f(3) + f(2)``` 我們得出一個結論:對於第N(N > 2)級臺階,其表示式為```f(N) = f(N-1) + f(N-2)```。那麼我們在結算的過程中,每次都記錄下```f(N-1)```和```f(N-2)```的值,逐級遷移這個值,就可以得到```f(N)```了。 現在```climbStairs```程式碼如下: ``` /** * @method climbStairs * @description 爬樓梯 * @param {number} n 樓梯臺階數 * @return {number} 一共有多少種走法 */ function climbStair (n) { // 通過觀察,我們可只第1級和第2級都是返回對應的n if (n <= 2) { return n; } else { // 對於n > 2的情況 let i = 1; // 初始存放第1級臺階的走法,對應的是f(N-2) let j = 2; // 初始存放第2級臺階的走法,對應的是f(N-1); // 定義走法num let num; // 從第3級開始,執行迴圈操作 for (let k = 3; k <= n; k++) { // f(N) = f(N-1) + f(N-2) num = i + j; // 同時移動 // 將f(N-1)的值給f(N-2) i = j; // 將當前值給f(N-1) j = num; } // 返回結果 return num; } } ``` > 這一次我們直接在時間複雜度上降低了,變成了```O(N)```,執行起來更加是和那啥一樣,流暢、順滑~ 我們來看下測試效果,連續執行三次測試結果如下: |序號|結果|執行時間| |----|----|----| |1| 165580141 | 6.570ms | |2| 165580141 | 6.647ms | |3| 165580141 | 6.658ms | > 相對於```遞迴```的實現方式,```遞推```的實現從時間複雜度上更低,執行也會更高效~ 此時此刻,這個爬樓梯的問題終於是回答圓滿了,這個時候面試官看你就會像```丈母孃看女婿一樣,怎麼看怎麼可愛```。 動態規劃的演算法問題有很多種不同的形式,爬樓梯是其中的一種。在這裡胡哥要給大家留一道面試題啦,看看大家對動態規劃是不是有了深刻的理解。 面試真題如下: 你是一個有信仰的強盜,有一排房屋等待你去搶劫,在搶劫中不能相鄰的房屋不能搶,只能間隔一個或多個房屋進行搶劫,房屋中錢財都是非負整數,資料格式如下:```[3, 4, 5, 2, 1, 1]```,請計算出你能搶到的最大金額是多少。 > 這個強盜相當有信仰,竟然不都搶走~ 歡迎各位小夥伴留言,談談你對動態規劃的理解,留下這道面試題的答案,一起來探討交流~ ## 後記 以上就是胡哥今天給大家分享的內容,喜歡的小夥伴記得```點贊```、```收藏```呦,關注胡哥有話說,學習前端不迷路,歡迎多多留言交流... > 胡哥有話說,專注於大前端技術領域,分享前端系統架構,框架實現原理,最新最高效的技術實踐!