本篇博客參考 快樂動起來呀老師的《js版數據結構與算法》Part1改編自《漫畫算法》原做者:程序員小灰javascript
而近兩年隨着互聯網行業競爭愈發激烈,市場上對前端崗位的算法要求也有必定的提高。前端
我記得大三參加騰訊的校招面試時只准備了幾種常見的排序算法就足以應對了,然而今年包括今日頭條在內的多家大廠的前端筆試題目中都出現了"貪心算法""動態規劃""分治算法"等進階性的算法題目。若是在沒有提早準備的狀況下現場應對這類進階性的算法題目並無那麼簡單。java
本篇博客將分爲兩個部分程序員
一一一一一一一一一一一一一一一一一一一一一一一一一面試
題目:算法
好比,每次放1本書,一共放10次,這是其中一種方法。咱們能夠簡寫成 1,1,1,1,1,1,1,1,1,1。
數組
再好比,每次放2本書,一共放5次,這是另外一種方法。咱們能夠簡寫成 2,2,2,2,2。
緩存
固然,除此以外,還有不少不少種方式。
數據結構
一一一一一一一一一一一一一一一一一一一一一一一一一函數
第一種:
第二種:
這裏爲了方便你們理解,我再另外舉一個例子:
如圖所示 假設只能經過road1或road2這兩條路徑到達終點
(至關於咱們把放書的最後一步分爲放2本和放1本兩種狀況)
到達road1有x條路經(至關於0到8本的放法數量F(8))
到達road2有y條路經(至關於0到9本的放法數量F(9))
那麼到達終點的可能性就是 x+y了 (也就是咱們前面推導的 F(10) = F(9)+F(8) )
F(1) = 1;
F(2) = 2;
F(n) = F(n-1)+F(n-2)(n>=3)
相信你們看完必定對動態規劃有了一個初步的認識,
這裏你們能夠本身先嚐試寫一下這道題的代碼
接下來咱們先來經過一道LeetCode實戰原題加深咱們對動態規劃的理解
LeetCode第63題 原題地址
題目難度 中等
題目描述
一個機器人位於一個m x n網格的左上角 (起始點在下圖中標記爲「Start」 )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲「Finish」)。
如今考慮網格中有障礙物。那麼從左上角到右下角將會有多少條不一樣的路徑?
![]()
網格中的障礙物和空位置分別用1
和0
來表示。
說明:m和n的值均不超過 100。
實例1
輸入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
輸出: 2
解釋: 3x3 網格的正中間有一個障礙物。
從左上角到右下角一共有 2 條不一樣的路徑:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
相信你們已經看出來了,咱們這道題與咱們漫畫中演示的題目幾乎一致。
但它又提高了一點難度,咱們須要考慮到障礙物的狀況。
還記得咱們以前提到的動態規劃三要素【最優子結構】【邊界】和【狀態轉移公式】嗎?
拿題目中給出的圖片進行舉例:
在不考慮障礙物的狀況下,咱們利用動態規劃的思想,到達終點有幾種狀況呢?
很明顯是兩種: 從終點上方或終點左方到達
7 * 3 矩陣
那咱們很容易得出這個7*3的矩陣的終點 F(7*3) 的最優子結構爲 F(6*3) 和 F(7*2)
至此它的狀態轉移公式也一目瞭然: F(m*n) = F(m-1*n) + F(m*n-1)
最後咱們考慮一下它的邊界狀況:
通過評論區同窗的指正,其實咱們以前考慮的F(2*2)邊界狀況繼續往下分也能夠分爲一列和一行即F(1*2) + F(2*1)兩種狀況。
全部的F(m*n)的矩陣最後均可以拆分爲一行和一列的狀況,因此咱們這裏邊界狀況只有兩種。
export default (arr) => {
let dp = (m, n) => {
// 檢查起始或者目標元素是否是1(障礙物),若是起始或者最後那個格就是1,說明怎麼都怎麼不到那,
// 直接返回0
if (arr[m - 1][n - 1] === 1 || arr[0][0] === 1) {
return 0
}
// 有邊界
if (m < 2 || n < 2) {
// 第一種邊界 1行n列
if (m < 2) {
return arr[m - 1].includes(1) ? 0 : 1
} else {
// 第二種邊界 n行1列
for (let i = 0; i < m; i++) {
if (arr[i][0] === 1) {
return 0
}
}
return 1
}
} else {
// 遞歸
return dp(m - 1, n) + dp(m, n - 1)
}
}
return dp(arr.length, arr[0].length)
}複製代碼
感謝同窗們在評論區提出的問題
首先說明咱們上方代碼是沒有問題的,可是在LeetCode上的第27個測試用例上超出了時間限制
這個測試用例相對複雜,是一個33*22的二維矩陣
那爲何矩陣到達必定長度時咱們的方法時間複雜度會太高呢?
咱們先回顧一下咱們以前的思路:
將F(9)分解後那麼F(10) 能夠寫成
而F(8) 又= F(7) + F(6)
那麼繼續將F(8)分解 F(10) 能夠寫成
注意到了嗎?
越向下劃分重複的就越多,可能你會以爲不就是多加一次F(n)的值嗎
可是這裏我必需要提醒你的是:
F(n)不單純是一個值的引用,他是一個遞歸函數,咱們每重複一次它都會從新執行一次F函數
咱們不討論時間複雜度具體怎樣計算
但這裏我能夠告訴你們咱們以前的方法時間複雜度是O(2^n)
那麼怎樣改進呢?
在這裏提出兩個思路,你們也能夠嘗試本身寫一下:
// 傳入二維數組
arr => {
// 行數
let n = arr.length;
if(!n){
return 0;
}
// 列數
let m = arr[0].length;
// 起點或終點爲障礙物
if(arr[0][0] === 1 || arr[n - 1][m - 1] === 1){
return 0;
}
// 記錄到達每一個位置的路徑可能數
var rode= [];
// 遍歷每一行
for(let i = 0; i < n; i++){
rode[i] = []; // 遍歷每一行的每一個元素
for(let j = 0; j < m; j++){
// 若某節點是障礙物,則通向該節點的路徑數量爲0
if(arr[i][j] === 1){
rode[i][j] = 0;
} else if(i === 0){
// 如果第一行 每一個節點是否能經過都依賴它左方節點
rode[i][j] = rode[i][j - 1] === 0 ? 0 : 1;
} else if(j === 0){
// 如果第一列 每一個節點是否能經過都依賴它上方節點
rode[i][j] = rode[i - 1][j] === 0 ? 0 : 1;
} else {
// 不然遞歸
rode[i][j] = rode[i - 1][j] + rode[i][j - 1];
}
}
}
return rode[n - 1][m - 1];
}複製代碼
你們發現了嗎,當你掌握了動態規劃的三要素【最優子結構】【邊界】和【狀態轉移公式】
後,解決動態規劃的算法題目並非很難。可是其中的思想是須要咱們好好消化吸取的。
相信之後遇到這類問題你也能夠迎刃而解。