爬樓梯這個問題也是一個很經典的面試題,可以換各種人物動物,比如青蛙、小兔子跳臺階,張三李四爬樓梯等等。
題目會類似於下面這樣:
假設你正在爬樓梯,需要 n 階你才能到達樓頂,每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
假設有 2 個臺階,那麼有兩種方法可以爬到樓頂:
- 1 個臺階 + 1 個臺階
- 2 個臺階
假設有 3 個臺階,那麼有三種方法可以爬到樓頂:
- 1 個臺階 + 1 個臺階 + 1 個臺階
- 1 個臺階 + 2 個臺階
- 2 個臺階 + 1 個臺階
假設有 5 個臺階,那麼有八種方法可以爬到樓頂:
- 1 個臺階 + 1 個臺階 + 1 個臺階 + 1 個臺階 + 1 個臺階
- 1 個臺階 + 1 個臺階 + 1 個臺階 + 2 個臺階
- 1 個臺階 + 2 個臺階 + 2 個臺階
- 2 個臺階 + 2 個臺階 + 1 個臺階
- 2 個臺階 + 1 個臺階 + 2 個臺階
- 2 個臺階 + 1 個臺階 + 1 個臺階 + 1 個臺階
- 1 個臺階 + 2 個臺階 + 1 個臺階 + 1 個臺階
- 1 個臺階 + 1 個臺階 + 2 個臺階 + 1 個臺階
有了三個例子,我們來倒推一下,變成程式碼的形式。
假設爬到樓頂的方法為f(n)
,根據題目意思有兩種可能
- 爬一個臺階:
f(n-1)
- 爬兩個臺階:
f(n−2)
只有這兩種可能,將這兩種可能相加,可以得到遞推公式:
f(n) = f(n-1) + f(n-2)
這樣我們就可以使用遞迴來實現:
但在遞迴時也需要處理邊界問題,比如f(1) = 1
,f(2) = 2
,這兩種情況我們可以直接返回,而不需要遞迴。
所以可以得到這樣的遞迴函式:
function climbStairs($n) {
if ($n == 1) return 1;
if ($n == 2) return 2;
return climbStairs($n - 1) + climbStairs($n - 2);
}
echo climbStairs(2); // 2
echo climbStairs(3); // 3
echo climbStairs(5); // 8
再來看看使用迴圈是否能解決問題?
假設我們使用迴圈來實現,那麼可以使用一個陣列來儲存已經計算過的值,假設使用一個陣列dp[]
來儲存爬一個臺階有多少種方法:
dp[n] = dp[n-1] + dp[n-2]
和遞迴一樣,也滿足dp[1] = 1
, dp[2] = 2
,可以得到以下函式
function climbStairs($n) {
$dp = [];
$dp[1] = 1;
$dp[2] = 2;
for ($i = 3; $i <= $n; $i++) {
$dp[$i] = $dp[$i - 1] + $dp[$i - 2];
}
return $dp[$n];
}
在使用遞迴時,我們是從最後一個臺階開始,朝著底下的臺階去找,並不確實下一個是不是終點,只能一個一個去找。
而在使用迴圈時,我們是從第一個臺階開始,往上爬,這個時候是知道前面的方法個數的,直接拿來用就好了,不用再重新算了。
echo climbStairs(3), PHP_EOL; // 3
echo climbStairs(4), PHP_EOL; // 5
echo climbStairs(5), PHP_EOL; // 8
echo climbStairs(6), PHP_EOL; // 13
echo climbStairs(7), PHP_EOL; // 21
echo climbStairs(8), PHP_EOL; // 34
上面的列舉也可以驗證我們的猜想,實際上我們只需要知道:當前方法個數 = 前一個方法個數 + 前前一個方法個數
。
假設f
為當前方法個數,a
和b
分別為前一個
和前前一個
的方法個數,可以得到一個新的迴圈方式:
我們是從第 0
級開始爬的,從第 0
個到第 0
個臺階,我們也可以當成一種方法,即f=1
function climbStairs($n) {
$a = $b = 0;
$f = 1;
for ($i = 1; $i <= $n; $i++) {
$a = $b;
$b = $f;
$f = $a + $b;
}
return $f;
}
以上就是一個動態規劃的程式碼,直接看的話可能比較難懂為什麼這麼寫,經過遞迴和迴圈的驗證與理解,看上去就很簡單了。
在面試中遇到演算法題時不要慌,可以先從簡單暴力的方法入手,再嘗試有沒有最優解。
同時也建議有空時可以去力扣刷刷演算法題,理解一些相關概念和思路,這樣就不會在遇到演算法題時恐慌,不知道該從何處入手了。
除此之外也要熟悉所用的語言,特別是內建的一些函式方法,比如驗證一個數是不是迴文數時,PHP 就可以使用 strrev 函式,將它進行反轉後再進行比較就可以直接判斷是與否了。
$n1 = 21;
$n2 = 22;
$n3 = -22;
var_dump(strrev($n1) == $n1);
var_dump(strrev($n2) == $n2);
var_dump(strrev($n3) == $n3);
面試後記得及時覆盤,下次肯定比這次更好~
本文參與了 SegmentFault 思否徵文「如何“反殺”面試官?」,歡迎正在閱讀的你也加入。