文章首發於:github.com/USTB-musion…前端
今天來聊一聊前端面試中出現頻率很是高的一種算法思想——「遞歸」。git
先看下幾個常見的面試題:github
你能夠先思考一下如何回答上邊的問題🤔,而後帶着答案來閱覽接下來的內容。面試
遞歸思想在前端面試中很是常見,除了上面的一些題目以外,二叉樹的前中後序遍歷,斐波那契數列等都用到了遞歸的思想。簡單地理解遞歸就是:本身調用本身。那如何編寫遞歸的代碼呢❓ 在筆者看來:算法
主要有兩個關鍵步驟:數組
先來看個簡單的例子:如何求1+2+3+4+...+n的和?相信用for循環的方法你們都知道如何編寫:bash
function sum(n) {
var total = 0
for (int i = 1; i <= n; i++) {
total = total + i
}
return total
}
複製代碼
那如何改成遞歸的寫法呢?函數
第一步: 寫出遞歸公式性能
細心觀察就會發現,其實就是n與n-1和n-2的關係優化
sum(n) = sum(n-1) + n
···
···
···
sum(100) = sum(99) + 100
sum(99) = sum(98) + 99
···
···
···
sum(5) = sum(4) + 5
sum(4) = sum(3) + 4
sum(3) = sum(2) + 3
sum(2) = sum(1) + 2
sum(1) = 1
複製代碼
將如上轉化成遞歸公式就是
function sum(n) {
return sum(n-1) + n
}
複製代碼
第二步:找出終止條件
遞歸公式寫出來了,那麼遞歸代碼就完成了一大半。如今來看一下上述問題的終止條件是什麼呢,即sum(1)= 1;
結合遞歸公式和終止條件,1+2+3+4+...+n求和的遞歸代碼以下:
function sum(n) {
if( n ===1 ) return 1
return sum(n-1) + n
}
複製代碼
假如樓梯有n個臺階,每次能夠走1個或2個臺階,請問走完這n個臺階有幾種走法❓
按照咱們上面的思路,先寫出遞歸公式。那這道題咱們如何去找出遞歸公式呢。假設有3個臺階,咱們能夠有3種走法:
1 1 1
1 2
2 1
複製代碼
第一種是每次都是走1個臺階。第二種是第一步走1個臺階,第二步走2個臺階。第三種是第一步走2個臺階,第二步走1個臺階。
寫出遞歸公式:
那若是有n個臺階呢?咱們認真思考下就會發現,第1步的走法有兩類:第一種是第一步走1個臺階,第二種是第二步走2個臺階。因此n個臺階的走法就能夠分爲:走完1個臺階後的n-1種走法,加上走完2個臺階後的n-2種走法,用遞歸公式表示就是:
function climbStairs(n) {
return climbStairs(n - 1) + climbStairs(n - 2)
}
複製代碼
找到終止條件:
climbStairs(n) = climbStairs(n-1) + climbStairs(n-2)
climbStairs(n-1) = climbStairs(n-2) + climbStairs(n-3)
···
···
···
climbStairs(5) = climbStairs(4) + climbStairs(3)
climbStairs(4) = climbStairs(3) + climbStairs(2)
climbStairs(3) = climbStairs(2) + climbStairs(1)
climbStairs(2) = 2
climbStairs(1) = 1
複製代碼
從上面的推導能夠看出:終止條件爲:
climbStairs(2) = 2
climbStairs(1) = 1
複製代碼
綜上所述,解決爬樓梯的代碼以下:
function climbStairs(n) {
if (n == 1) return 1
if (n == 2) return 2
return climbStairs(n-1) + climbStairs(n-2)
}
複製代碼
固然,能夠對上述題目作一個memorize操做,性能會好不少:
var calculated = []
function climbStairs(n) {
if(n == 1) {
return 1
}else if (n == 2) {
return 2
}else {
if(!calculated[n-1]){
calculated[n-1] = climbStairs(n-1)
}
if(!calculated[n-2]){
calculated[n-2] = climbStairs(n-2)
}
return calculated[n-1] + calculated[n-2]
}
}
複製代碼
解決完爬樓梯問題以後,思考下斐波那契數列問題的求解,有木有發現是同樣的問題和思路:)
如何用遞歸思想實現深拷貝❓若是要實現深拷貝那麼就須要考慮將對象的屬性, 與屬性的屬性,都拷貝過來, 這種狀況下就很是適合使用遞歸思想來解決,在拷貝的適合判斷屬性值的類型,若是是對象則遞歸調用deeplClone函數,不然直接返回該屬性值:
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
// // 根據obj的類型判斷是新建一個數組仍是對象
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
複製代碼
如何用遞歸思想實現數組的扁平化❓即如何把[1, [2], [3, [4, [5]]]]拍平獲得[1,2,3,4,5]❓
const flatten = (arr) => {
let result = [];
arr.forEach((item, i, arr) => {
// 若爲數組,遞歸調用 faltten,並將結果與result合併
if (Array.isArray(item)) {
result = result.concat(flatten(item));
} else {
result.push(arr[i])
}
})
return result;
};
const arr = [1, [2, [3, 4, 5]]];
console.log(flatten(arr)); // [1, 2, 3, 4, 5]
複製代碼
編寫遞歸代碼的關鍵在於找出遞歸公式和終止條件,最後將它們翻譯成代碼。遞歸代碼雖然比較簡潔。可是也有不少弊端,如性能不是很高效,這時候要作memorize等一些操做來優化性能。容易產生堆棧溢出等問題,因此咱們在寫代碼的時候要注意這些。