一文了解分而治之和动态规则算法在前端中的应用
一文了解分而治之和動(dòng)態(tài)規(guī)則算法
- 一、分而治之
- 1、分而治之是什么?
- 2、應(yīng)用場(chǎng)景
- 3、場(chǎng)景剖析:歸并排序和快速排序
- 二、動(dòng)態(tài)規(guī)則
- 1、動(dòng)態(tài)規(guī)則是什么?
- 2、應(yīng)用場(chǎng)景
- 3、場(chǎng)景剖析:斐波那契數(shù)列
- 4、動(dòng)態(tài)規(guī)則VS分而治之
- 三、分而治之算法常見(jiàn)應(yīng)用
- 1、leetcode 374:猜數(shù)字大小
- 2、leetcode 226:翻轉(zhuǎn)二叉樹
- 3、leetcode 100:相同的樹
- 4、leetcode 101:對(duì)稱二叉樹
- 四、動(dòng)態(tài)規(guī)則算法常見(jiàn)應(yīng)用
- 1、leetcode 70:爬樓梯
- 2、leetcode 198:打家劫舍
- 3、leetcode 62:不同路徑
- 五、結(jié)束語(yǔ)
眾多周知,分而治之算法和動(dòng)態(tài)規(guī)則算法是前端面試中的“寵兒”。而在我們的日常生活中,這兩個(gè)場(chǎng)景的應(yīng)用也相對(duì)比較廣泛。比如,分而治之算法常用于翻轉(zhuǎn)二叉樹、快速搜索等場(chǎng)景中,而動(dòng)態(tài)規(guī)則算法,則常用于最少硬幣找零問(wèn)題、背包問(wèn)題等場(chǎng)景中。
在下面的這篇文章中,將講解分而治之和動(dòng)態(tài)規(guī)則的常用場(chǎng)景以及對(duì) leetcode 的一些經(jīng)典例題進(jìn)行解析。
一、分而治之
1、分而治之是什么?
- 分而治之是算法設(shè)計(jì)中的一種方法。
- 它將一個(gè)問(wèn)題分成多個(gè)和原問(wèn)題相似的小問(wèn)題,遞歸解決小問(wèn)題再將結(jié)果合并以解決原來(lái)的問(wèn)題。
2、應(yīng)用場(chǎng)景
- 歸并排序
- 快速搜索
- 二分搜索
- 翻轉(zhuǎn)二叉樹
- ……
3、場(chǎng)景剖析:歸并排序和快速排序
(1)場(chǎng)景一:歸并排序
- 分:把數(shù)組從中間一分為二。
- 解:遞歸地遞歸的對(duì)兩個(gè)子數(shù)組進(jìn)行歸并排序。
- 合:合并有序子數(shù)組。
(2)場(chǎng)景二:快速排序
- 分:選基準(zhǔn),按照基準(zhǔn)把數(shù)組分成兩個(gè)子數(shù)組。
- 解:遞歸地對(duì)兩個(gè)子數(shù)組進(jìn)行快速排序。
- 合:對(duì)兩個(gè)子數(shù)組進(jìn)行合并。
二、動(dòng)態(tài)規(guī)則
1、動(dòng)態(tài)規(guī)則是什么?
- 動(dòng)態(tài)規(guī)則是算法設(shè)計(jì)中的一種方法;
- 它將一個(gè)問(wèn)題分解為相互重疊的子問(wèn)題,通過(guò)反復(fù)求解子問(wèn)題,來(lái)解決原來(lái)的問(wèn)題。
看到這里,很多小伙伴會(huì)想著,動(dòng)態(tài)規(guī)則和分而治之不是解決同樣的問(wèn)題嗎?其實(shí)不是的。
注意:
-
動(dòng)態(tài)規(guī)則解決相互重疊的子問(wèn)題。
-
分而治之解決的是相互獨(dú)立的子問(wèn)題。
這樣說(shuō)可能還有點(diǎn)抽象,稍后將在第3點(diǎn)的時(shí)候做詳細(xì)解析。
2、應(yīng)用場(chǎng)景
- 最少硬幣找零問(wèn)題
- 背包問(wèn)題
- 最長(zhǎng)公共子序列
- 矩陣鏈相乘
- ……
3、場(chǎng)景剖析:斐波那契數(shù)列
斐波那契數(shù)列是一個(gè)很典型的數(shù)學(xué)問(wèn)題。斐波那契數(shù)列指的是這樣一個(gè)數(shù)列:
這個(gè)數(shù)列從第3項(xiàng)開(kāi)始,每一項(xiàng)都等于前兩項(xiàng)之和。即:
Fibonacci[n]={0,n=01,n=1Fibonacci[n?1]+Fibonacci[n?2],n>1Fibonacci[n]= \begin{cases} 0,n=0 \\ 1,n=1 \\ Fibonacci[n-1]+Fibonacci[n-2],n>1 \end{cases} Fibonacci[n]=??????0,n=01,n=1Fibonacci[n?1]+Fibonacci[n?2],n>1?
那么我們來(lái)梳理一下,斐波那契數(shù)列是怎么運(yùn)用動(dòng)態(tài)規(guī)則算法的。主要有以下兩點(diǎn):
- 定義子問(wèn)題:F(n)=F(n - 1) + F(n - 2);
- 反復(fù)執(zhí)行:從2循環(huán)到n,執(zhí)行上述公式。
4、動(dòng)態(tài)規(guī)則VS分而治之
看完上面的內(nèi)容,我們來(lái)梳理下動(dòng)態(tài)規(guī)則和分而治之的區(qū)別。先用一張圖展示兩者的區(qū)別。
大家可以看到,左邊的斐波那契數(shù)列是將所有問(wèn)題分解為若干個(gè)相互重疊的問(wèn)題,每個(gè)問(wèn)題的解法都一樣。
而右邊的翻轉(zhuǎn)二叉樹,左右子樹是相互獨(dú)立的,需先翻轉(zhuǎn)左右子樹,且在翻轉(zhuǎn)過(guò)程中,它們各自翻轉(zhuǎn),互不干擾,左子樹干左子樹的活,右子樹干右子樹的活。
不像斐波那契數(shù)列那樣,每一層都是相互依賴的,一層嵌套一層,相互重疊。
這就是動(dòng)態(tài)規(guī)則和分而治之的區(qū)別。
三、分而治之算法常見(jiàn)應(yīng)用
引用leetcode的幾道經(jīng)典題目來(lái)強(qiáng)化分而治之算法。
1、leetcode 374:猜數(shù)字大小
(1)題意
這里附上原題鏈接
猜數(shù)字游戲的規(guī)則如下:
- 每輪游戲,我都會(huì)從 1 到 n 隨機(jī)選擇一個(gè)數(shù)字。 請(qǐng)你猜選出我選的是哪個(gè)數(shù)字。
- 如果你猜錯(cuò)了,我會(huì)告訴你,你猜測(cè)的數(shù)字比我選出的數(shù)字是大了還是小了。
你可以通過(guò)調(diào)用一個(gè)預(yù)先定義好的接口 int guess(int num) 來(lái)獲取猜測(cè)結(jié)果,返回值一共有 3 種可能的情況(-1,1 或 0):
- -1 :我選出的數(shù)字比你猜的數(shù)字小 pick < num 。
- 1 :我選出的數(shù)字比你猜的數(shù)字大 pick > num 。
- 0 :我選出的數(shù)字和你猜的數(shù)字一樣。恭喜!你猜對(duì)了!pick == num 。
返回我選出的數(shù)字。
(2)解題思路
- 二分搜索,同樣具備“分、解、合”的特性。
- 考慮選擇分而治之。
(3)解題步驟
- 分:計(jì)算中間元素,分割數(shù)組。
- 解:遞歸地在較大或者較小的數(shù)組進(jìn)行二分搜索。
- 合:不需要此步,因?yàn)樵谧訑?shù)組中搜到就返回了。
(4)代碼實(shí)現(xiàn)
/** * Forward declaration of guess API.* @param {number} num your guess* @return -1 if num is lower than the guess number* 1 if num is higher than the guess number* otherwise return 0* var guess = function(num) {}*//*** @param {number} n* @return {number}*/ let guessNumber = function(n) {const rec = (low, high) => {if(low > high){return;}// 1.計(jì)算中間元素,分割數(shù)組const mid = Math.floor((low + high) / 2);// 2.與猜測(cè)的數(shù)字進(jìn)行比較const res = guess(mid);// 3.遞歸地在較大或者較小子數(shù)組進(jìn)行二分搜索if(res === 0){return mid;}else if(res === 1){return rec(mid + 1, high);}else{return rec(low, mid - 1);}}return rec(1, n); };2、leetcode 226:翻轉(zhuǎn)二叉樹
(1)題意
這里附上原題鏈接
翻轉(zhuǎn)一棵二叉樹。
(2)解題思路
- 先翻轉(zhuǎn)左右子樹,再將子樹換個(gè)位置。
- 符合“分、解、合”特性。
- 考慮選擇分而治之。
(3)解題步驟
- 分:獲取左右子樹。
- 解:遞歸地翻轉(zhuǎn)左右子樹。
- 合:將翻轉(zhuǎn)后的左右子樹換個(gè)位置放到根節(jié)點(diǎn)上。
(4)代碼實(shí)現(xiàn)
/*** Definition for a binary tree node.* function TreeNode(val, left, right) {* this.val = (val===undefined ? 0 : val)* this.left = (left===undefined ? null : left)* this.right = (right===undefined ? null : right)* }*/ /*** @param {TreeNode} root* @return {TreeNode}*/var invertTree = function(root) {if(!root){return null;}return{//1.根節(jié)點(diǎn)值不變val:root.val,//2.遞歸地將左子樹與右子樹結(jié)點(diǎn)變換left:invertTree(root.right),//3.遞歸地將右子樹與左子樹結(jié)點(diǎn)變換right:invertTree(root.left)} };3、leetcode 100:相同的樹
(1)題意
這里附上原題鏈接
給你兩棵二叉樹的根節(jié)點(diǎn) p 和 q ,編寫一個(gè)函數(shù)來(lái)檢驗(yàn)這兩棵樹是否相同。
如果兩個(gè)樹在結(jié)構(gòu)上相同,并且節(jié)點(diǎn)具有相同的值,則認(rèn)為它們是相同的。
(2)解題思路
- 兩棵樹:根節(jié)點(diǎn)的值相同,左子樹相同,右子樹相同。
- 符合“分、解、合”特性。
- 考慮選擇分而治之。
(3)解題步驟
- 分:獲取兩棵樹的左子樹和右子樹。
- 解:遞歸地判斷兩棵樹的左子樹是否相同,右子樹是否相同。
- 合:將上述結(jié)果合并,如果根節(jié)點(diǎn)的值也相同,兩棵樹就相同。
(4)代碼實(shí)現(xiàn)
/*** Definition for a binary tree node.* function TreeNode(val, left, right) {* this.val = (val===undefined ? 0 : val)* this.left = (left===undefined ? null : left)* this.right = (right===undefined ? null : right)* }*/ /*** @param {TreeNode} p* @param {TreeNode} q* @return {boolean}*/ let isSameTree = function(p, q) {if(!p && !q){return true;}/*** 判斷條件:* 1.p樹和q樹同時(shí)存在;* 2.每遍歷一個(gè)節(jié)點(diǎn),兩棵樹的節(jié)點(diǎn)值都存在;* 3.遞歸左子樹,比較每個(gè)節(jié)點(diǎn)值;* 4.遞歸右子樹,比較每個(gè)節(jié)點(diǎn)值。*/if(p && q && p.val === q.val &&isSameTree(p.left, q.left) &&isSameTree(p.right, q.right)){return true;}return false; };4、leetcode 101:對(duì)稱二叉樹
(1)題意
這里附上原題鏈接
給定一個(gè)二叉樹,檢查它是否是鏡像對(duì)稱的。
(2)解題思路
- 轉(zhuǎn)化為:左右子樹是否鏡像。
- 分解為:樹1的左子樹和樹2的右子樹是否鏡像,樹1的右子樹和樹2的左子樹是否鏡像。
- 符合“分、解、合”特性,考慮選擇分而治之。
(3)解題步驟
- 分:獲取兩棵樹的左子樹和右子樹。
- 解:遞歸地判斷樹1的左子樹和樹2的右子樹是否鏡像,樹1的右子樹和樹2的左子樹是否鏡像。
- 合:如果上述成立,且根節(jié)點(diǎn)值也相同,兩棵樹就鏡像。
(4)代碼實(shí)現(xiàn)
let isSymmetric = function(root){if(!root){return true;}const isMirror = (l, r) => {if(!l && !r){return true;}/*** 判斷條件:* 1.左子樹和右子樹同時(shí)存在;* 2.左子樹和右子樹的根節(jié)點(diǎn)相同;* 3.左子樹的左節(jié)點(diǎn)和右子樹的右節(jié)點(diǎn)鏡像相同;* 4.左子樹的右結(jié)點(diǎn)和右子樹的左結(jié)點(diǎn)鏡像相同*/if(l && r && l.val === r.val &&isMirror(l.left, r.right) &&isMirror(l.right, r.left)){return true;}return false;}return isMirror(root.left, root.right); }四、動(dòng)態(tài)規(guī)則算法常見(jiàn)應(yīng)用
引用leetcode的幾道經(jīng)典題目來(lái)強(qiáng)化動(dòng)態(tài)規(guī)則算法。
1、leetcode 70:爬樓梯
(1)題意
這里附上原題鏈接
假設(shè)你正在爬樓梯。需要 n 階你才能到達(dá)樓頂。
每次你可以爬 1 或 2 個(gè)臺(tái)階。你有多少種不同的方法可以爬到樓頂呢?
**注意:**給定 n 是一個(gè)正整數(shù)。
(2)解題思路
- 爬到第n階可以在第n - 1階爬1個(gè)臺(tái)階,或者在第n - 2階爬2個(gè)臺(tái)階。
- F(n) = F(n - 1) + F(n - 2)。
- 使用動(dòng)態(tài)規(guī)則。
(3)解題步驟
- 定義子問(wèn)題:F(n) = F(n - 1) + F(n - 2)。
- 反復(fù)執(zhí)行:從2循環(huán)到n,執(zhí)行上述公式。
(4)代碼實(shí)現(xiàn)
/** @param {number} n* @return {number}*/ // 數(shù)組方法 var climbStairs = function(n) {if(n < 2){return 1;}// 記錄第0階和第1階可以走多少步const dp = [1, 1];// 從第2階開(kāi)始遍歷,直至第5階for(let i = 2; i <= n; i++){dp[i] = dp[i - 1] + dp[i - 2];}return dp[n]; };如果dp用一維數(shù)組來(lái)記錄的話,時(shí)間復(fù)雜度和空間復(fù)雜度都為O(n),這樣子的話效率還是偏低的。
那么有什么方法可以來(lái)降低它的復(fù)雜度呢?
可以采用變量的方法。從上面的代碼中我們可以看出,dp的值用一個(gè)數(shù)組存著,一直在線性增長(zhǎng)。那么這個(gè)時(shí)候我們可以考慮把這個(gè)一維數(shù)組變換成單變量的形式,不斷進(jìn)行替換,來(lái)降低空間復(fù)雜度。
下面用代碼實(shí)現(xiàn)一遍。
let climbStairs2 = function(n){if(n < 2){return 1;}//定義一個(gè)變量,記錄 n - 2 時(shí)的臺(tái)階數(shù)let dp0 = 1;//定義一個(gè)變量,記錄 n - 1 時(shí)的臺(tái)階數(shù)let dp1 = 1;for(let i = 2; i <= n; i++){const temp = dp0;//每遍歷一次,就讓dp0指向下一個(gè)數(shù)的值,即dp1dp0 = dp1;//每遍歷一次,就讓dp1指向dp1下一個(gè)數(shù)的值,即前兩個(gè)數(shù)的和,也就是dp1和原來(lái)dp0的值dp1 = dp1 + temp;}return dp1; }從上面的代碼中可以看出,沒(méi)有了數(shù)組或者像矩陣一樣線性增長(zhǎng)的數(shù)組,空間復(fù)雜度就變?yōu)榱薕(1)。
2、leetcode 198:打家劫舍
(1)題意
這里附上原題鏈接
你是一個(gè)專業(yè)的小偷,計(jì)劃偷竊沿街的房屋。每間房?jī)?nèi)都藏有一定的現(xiàn)金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統(tǒng),如果兩間相鄰的房屋在同一晚上被小偷闖入,系統(tǒng)會(huì)自動(dòng)報(bào)警。
給定一個(gè)代表每個(gè)房屋存放金額的非負(fù)整數(shù)數(shù)組,計(jì)算你 不觸動(dòng)警報(bào)裝置的情況下 ,一夜之內(nèi)能夠偷竊到的最高金額。
(2)解題思路
- f(k) = 從前k個(gè)房屋中能偷竊到的最大數(shù)額。
- Ak = 第k個(gè)房屋的錢數(shù)。
- f(k) = max(f(k - 2) + Ak, f(k - 1))。
- 考慮使用動(dòng)態(tài)規(guī)則。
(3)解題步驟
- 定義子問(wèn)題:f(k) = max(f(k - 2) + Ak, f(k - 1))。
- 反復(fù)執(zhí)行:從2循環(huán)到n,執(zhí)行上述公式。
(4)代碼實(shí)現(xiàn)
/*** @param {number[]} nums* @return {number}*/let rob1 = function(nums) {if(nums.length === 0){return 0;}// 前0個(gè)房屋和前1個(gè)房屋能劫持到的金錢數(shù)const dp = [0,nums[0]];for(let i = 2; i <= nums.length; i++){dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);}return dp[nums.length]; };與爬樓梯同樣,如果dp用一維數(shù)組來(lái)記錄的話,時(shí)間復(fù)雜度和空間復(fù)雜度都為O(n),這樣子的話效率還是偏低的。
那這個(gè)時(shí)候就可以采用單變量的方法,來(lái)降低空間復(fù)雜度。
下面用代碼實(shí)現(xiàn)一遍。
let rob2 = function(nums) {if(nums.length === 0){return 0;}let dp0 = 0;let dp1 = nums[0];for(let i = 2; i <= nums.length; i++){const dp2 = Math.max(dp0 + nums[i - 1], dp1);dp0 = dp1;dp1 = dp2;}return dp1; };此時(shí)空間復(fù)雜度自然也就變?yōu)镺(1)了。
3、leetcode 62:不同路徑
(1)題意
這里附上原題鏈接
一個(gè)機(jī)器人位于一個(gè) m x n 網(wǎng)格的左上角 (起始點(diǎn)在下圖中標(biāo)記為 “Start” )。
機(jī)器人每次只能向下或者向右移動(dòng)一步。機(jī)器人試圖達(dá)到網(wǎng)格的右下角(在下圖中標(biāo)記為 “Finish” )。
問(wèn)總共有多少條不同的路徑?
(2)解題思路
- 每一步只能向下或者向右移動(dòng)一步,因此想要走到(i,j),如果向下走一步,那么從(i-1,j)走過(guò)來(lái);如果向右走一步,那么從(i,j-1)走過(guò)來(lái)。
- f(i, j) = f(i-1, j) + f(i, j-1)。
- 使用動(dòng)態(tài)規(guī)則。
(3)解題步驟
- 定義子問(wèn)題:f(i, j) = f(i-1, j) + f(i, j-1)。
- 反復(fù)執(zhí)行:從2循環(huán)到n,執(zhí)行上述公式。
(4)代碼實(shí)現(xiàn)
let uniquePaths = function(m, n){const f = new Array(m).fill(0).map(() => new Array(n).fill(0));for(let i = 0; i < m; i++){// 將第一列全部補(bǔ)上1f[i][0] = 1;}for(let j = 0; j < n; j++){// 將第一行全部補(bǔ)上1f[0][j] = 1;}for(let i = 1; i < m; i++){for(let j = 1; j < n; j++){f[i][j] = f[i - 1][j] + f[i][j - 1];}}return f[m - 1][n - 1]; }五、結(jié)束語(yǔ)
分而治之和動(dòng)態(tài)規(guī)則算法在前端中的應(yīng)用還是挺多的,特別是在面試或筆試的時(shí)候會(huì)經(jīng)常出現(xiàn)這類題目,大家可以在此之外再繼續(xù)多刷刷此類 leetcode 的題,做多了慢慢就能舉一反三了~
- 關(guān)注公眾號(hào) 星期一研究室 ,第一時(shí)間關(guān)注學(xué)習(xí)干貨,更多有趣的專欄待你解鎖~
- 如果這篇文章對(duì)你有用,記得點(diǎn)個(gè)贊加個(gè)關(guān)注再走哦~
- 我們下期見(jiàn)!🥂🥂🥂
總結(jié)
以上是生活随笔為你收集整理的一文了解分而治之和动态规则算法在前端中的应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 樱桃推出 KC 200 MX 机械键盘:
- 下一篇: 一文了解贪心算法和回溯算法在前端中的应用