最近刷leetcode 79題 Word Search需要用到DFS算法,由於是刷leetcode,心想不能用遞歸,影響效率,於是利用stack完成。之後又利用遞歸(使用cache)實現了一次,結果竟然是遞歸的算法比非遞歸更快。
「低效」的遞歸
對於遞歸,通常會有效率低下的映像,一般是因爲2點:
- 重複計算
- 函數調用開銷
對於重複計算,可以緩存計算結果來解決。
對於函數調用開銷,可以利用「尾遞歸」來解決,不過目前的v8引擎並沒有實現對尾遞歸的優化,所以最開始我以爲遞歸沒有理由比非遞歸更快。
遞歸與堆棧
非遞歸的DFS算法使用一個「堆棧」來實現。而同樣,函數調用也是利用「棧」來完成。
首先,Javascipt並沒有原生的堆棧這個數據結構,通常是利用數組來實現,效率上應該會有所損失。
其次,系統堆棧快於手動堆棧是沒有疑問的,而且系統堆棧使用的是寄存器,比內存要快很多。
最後,函數調用會有額外開銷這個是沒法避免的,但是當函數變量不多,遞歸層級不深的時候,這個開銷帶來的效率損失不能抵消系統堆棧帶來的效率提升。
綜合來看,在不爆棧的情況下,大部分Javascript代碼裏使用了緩存的遞歸在算法效率上高於非遞歸算法,並且遞歸算法的表現力是完全高於非遞歸的。很多時候,出於臆斷進行的所謂優化,完全是負優化。
關於遞歸的隨想
之前在看SICP的時候,發現函數式編程沒有循環,非函數式語言的循環操作都是利用「遞歸」的形式來完成的。而且所有的遞歸,都可以改成迭代的形式,避免了遞歸重複計算的缺點,也無需使用緩存來加速遞歸的計算,省下了緩存的開銷,所以有句話叫做“所有循環都是尾遞歸”。
總結
- 慣性思維不可取,實踐檢驗真理
- 遞歸 !== 慢
- 以後圖的遍歷、樹的遍歷、巴拉巴拉其它情況,直接寫遞歸,誰懟我說遞歸效率低,就讓他來solo。(莫名的開心咋回事兒啊?)
- 以上關於爲什麼遞歸快的推理全是推斷,但是DFS非遞歸慢於遞歸是事實(Javascript中), 跪求大神給出準確解讀。