<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
遞迴作為一種演演算法在程式設計語言中廣泛應用。 一個過程或函數在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞迴策略只需少量的程式就可描述出解題過程所需要的多次重複計算,大大地減少了程式的程式碼量。遞迴的能力在於用有限的語句來定義物件的無限集合。需要注意的是,遞迴必須要用邊界條件,否則很容易導致死迴圈
但是遞迴函數並不容易一下子就能想的出來,所以我們可以先通過一個子問題來逐步延申。
問題一: 假設我們需要求1+2+3+...+100的值,我們很容易想出下面的程式碼
function calcNum(n) { let sum = 0 for (let i = 0; i <= 100; i++) { sum += i } return sum } console.log(calcNum()) // 5050
這樣的程式碼是不滿足於遞迴中,直接或者間接呼叫本身的定義。那麼如何變成遞迴版本呢?(任何的迴圈,都可以寫成遞迴)
這兩大要素都找到了,就很容易寫出下面的遞迴版本
function calcNum(n) { let sum = 0 function dfs(n) { if (n > 100) { return } sum += n n++ dfs(n) } dfs(n) return sum } console.log(calcNum(1)) // 5050
遞迴一定伴隨著回溯,那麼什麼是回溯呢?以上面的程式碼為例子,我們分別在這兩處地方輸出n的值
function calcNum(n) { let sum = 0 function dfs(n) { if (n > 100) { return } sum += n n++ console.log(n, '遞迴前的n') dfs(n) console.log(n, '遞迴後的n') } dfs(n) return sum }
毫無疑問,"遞迴前的n"會按照1-100輸出,而"遞迴後的n"則會100-1輸出,這就說明了一個很重要的知識點,遞迴函數是類似一個棧迭代的過程,它的值輸出的順序為先進後出。通俗一點說,遞迴函數後面的引數,會反轉輸出。
要想理解回溯的含義,最為經典的還是二元樹的遍歷。二元樹的遍歷,又分為前序遍歷,中序遍歷,後序遍歷,分別通過程式碼來感受一下這三種遍歷的方式。 前序遍歷
// 基本結構 const treeNode = { val: 1, left: null, right: { val: 2, left: { val: 3, left: null, right: null }, right: null } }
來看下leetcode 前序遍歷原題
const root = { val: 5, left: { val: 4, left: { val: 1, right: null, left: null }, right: { val: 2, right: null, left: null } }, right: { val: 6, left: { val: 7, left: null, right: null }, right: { val: 8, left: null, right: null } } } function getRoot(root) { const res = [] function dfs(root) { if (!root) { return } res.push(root.val) dfs(root.left) dfs(root.right) } dfs(root) return res } console.log(getRoot(root)) // 5 4 1 2 6 7 8
中序遍歷
function getRoot(root) { const res = [] function dfs(root) { if (!root) { return } dfs(root.left) res.push(root.val) dfs(root.right) } dfs(root) return res } console.log(getRoot(root)) // 1 4 2 5 6 7 8
後續遍歷
function getRoot(root) { const res = [] function dfs(root) { if (!root) { return } dfs(root.left) dfs(root.right) res.push(root.val) } dfs(root) return res } console.log(getRoot(root)) // 1 2 4 7 8 6 5
在寫遞迴的時候,時刻都要注意邊界,以上場景的邊界,則是找不到節點(節點為null)的時候,就退出。
通過輸出的結果可以得知以下規律:
而實現該規律的主要依據,是通過遞迴的回溯導致,我們以中序遍歷為例子:
dfs(root.left) res.push(root.val) dfs(root.right)
當第一個dfs(root.left)遞迴結束後,就會彈出'1'的節點,然後就進了dfs(root.right)的節點,發現是個null,說明這個dfs(root.right)遞迴結束,那麼此時則回到了'4'的節點,然後就進入了dfs(root.right)節點...
二元樹的遍歷,其實類比於我們常見操作選單樹,或著樹形結構的操作...
let tree = [ { id: '1', title: '節點1', children: [ { id: '1-1', title: '節點1-1' }, { id: '1-2', title: '節點1-2' } ] }, { id: '2', title: '節點2', children: [ { id: '2-1', title: '節點2-1' } ] } ]
當我們要尋找遍歷每個節點的時候,同樣需要注意邊界,當我們操作的資料沒有的時候或者不存在的時候,則退出當次遍歷。
function getRootData(tree) { const res = [] function dfs(tree) { if (!tree || tree.length === 0) { return res } for (let i = 0; i < tree.length; i++) { const t = tree[i] if (t.children && t.children.length > 0) { dfs(t.children) // 開始遞迴 } else { res.push(t.title) // ['節點1-1', '節點1-2', '節點2-1'] } } } dfs(tree) return res }
可能有人會有疑問,這也沒有利用到回溯的操作啊,那麼我就換個場景,假如我給個你節點的id,你幫我找出他所有的父節點,那麼你可能會怎麼操作呢?
const tree = [ { id: '1', title: '節點1', children: [ { id: '1-1', title: '節點1-1' }, { id: '1-2', title: '節點1-2' } ] }, { id: '2', title: '節點2', children: [ { id: '2-1', title: '節點2-1', children: [ { id: '2-1-1', title: '節點2-1-1' } ] } ] } ] function pathTree(tree, id) { const res = [] function dfs(tree, path) { if (!tree || tree.length === 0) { return } for (let i = 0; i < tree.length; i++) { const t = tree[i] path.push(t.id) if (path.includes(id)) { res.push(path.slice()) } if (t.children && t.children.length > 0) { dfs(t.children, path) } path.pop() // 路徑回溯 } } dfs(tree, []) return res } console.log(pathTree(tree, '2-1-1')) // [2,2-1,2-1-1]
其實以上核心的程式碼為path.pop(),為什麼需要這句程式碼呢?我們可以通過leetcode上的排列組合問題來進行討論。
經典的組合問題
以上面題目為例子,從1-4(n)的數位中,排列2(k)個數的組合。解這個題目,可以使用暴力的做法,巢狀for迴圈來完成該功能。
function combine(n) { const res = [] for (let i = 1; i <= n; i++) { for (let j = i + 1; j <= n; j++) { res.push([i, j]) } } return res } console.log(combine(4), 'res') // [1,2][1,3][1,4][2,3][3,4][2,4]
細心朋友就會發現,它巢狀for次數則是等於它排列(k)的次數,那麼我假如k的次數是10,或者20,那麼豈不是要巢狀10個或者20個for迴圈。這套程式碼寫下來,估計是個人都會暈了。在以上程式碼塊中也可以發現重複的子問題也就是for迴圈,它想要的結果則為當我們找個了k個數的時候就停止。那麼我們可以嘗試通過遞回來解決該問題(遞迴for迴圈),但是這樣的過程還是很抽象的,需要藉助圖例理解。(任何的組合問題,都可以理解成為n叉樹的遍歷)
function combine(n, k) { const res = [] function dfs(n, path, startIndex) { if (path.length === k) { res.push(path.slice()) return } for (let i = startIndex; i <= n; i++) { path.push(i) dfs(n, path, i + 1) path.pop() } } dfs(n, [], 1) return res }
當我們選擇到了[1,2]之後,則需要回到1的位置,因為這個時候需要選擇3選項,形成[1,3],那麼回到'1'的操作,就類似於二元樹遍歷回到父節點的操作,如果此時我們不操作,path.pop(),那麼此時就會形成了[1,2,3],這樣的結果明顯不是我們想要,所以在操作push "3"的過程,需要先把2給pop掉。而遞迴的終止條件則為當路徑的長度等於k的時候則退出。 另外在函數體中,還發現了startIndex的存在,這個是作為橫向for迴圈開始的位置,我們結合上面兩個for迴圈的程式碼,是不是發現了j = i + 1的操作,而這個startIndex則是還原了這個操作而已。
到此這篇關於JavaScript中關於遞迴與回溯的範例詳解的文章就介紹到這了,更多相關JavaScript遞迴 回溯內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45