JavaScript中關(guān)于遞歸與回溯的實(shí)例詳解
何為遞歸
遞歸作為一種算法在程序設(shè)計(jì)語言中廣泛應(yīng)用。 一個(gè)過程或函數(shù)在其定義或說明中有直接或間接調(diào)用自身的一種方法,它通常把一個(gè)大型復(fù)雜的問題層層轉(zhuǎn)化為一個(gè)與原問題相似的規(guī)模較小的問題來求解,遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復(fù)計(jì)算,大大地減少了程序的代碼量。遞歸的能力在于用有限的語句來定義對象的無限集合。需要注意的是,遞歸必須要用邊界條件,否則很容易導(dǎo)致死循環(huán)
構(gòu)成遞歸條件
- 子問題須與原始問題為同樣的事,且更為簡單
- 不能無限制地調(diào)用本身,須有個(gè)出口,化簡為非遞歸狀況處理
但是遞歸函數(shù)并不容易一下子就能想的出來,所以我們可以先通過一個(gè)子問題來逐步延申。
問題一: 假設(shè)我們需要求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
這樣的代碼是不滿足于遞歸中,直接或者間接調(diào)用本身的定義。那么如何變成遞歸版本呢?(任何的循環(huán),都可以寫成遞歸)
- 尋找相同的子問題 該題目相同的子問題很明顯是sum+=i,該過程是重復(fù)調(diào)用的過程。
- 尋找終止條件 尋找遞歸的終止條件,該問題的終止條件是i>100的情況
這兩大要素都找到了,就很容易寫出下面的遞歸版本
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
關(guān)于回溯
遞歸一定伴隨著回溯,那么什么是回溯呢?以上面的代碼為例子,我們分別在這兩處地方輸出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輸出,這就說明了一個(gè)很重要的知識點(diǎn),遞歸函數(shù)是類似一個(gè)棧迭代的過程,它的值輸出的順序?yàn)橄冗M(jìn)后出。通俗一點(diǎn)說,遞歸函數(shù)后面的參數(shù),會反轉(zhuǎn)輸出。
要想理解回溯的含義,最為經(jīng)典的還是二叉樹的遍歷。二叉樹的遍歷,又分為前序遍歷,中序遍歷,后序遍歷,分別通過代碼來感受一下這三種遍歷的方式。 前序遍歷
// 基本結(jié)構(gòu) 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
后續(xù)遍歷
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
在寫遞歸的時(shí)候,時(shí)刻都要注意邊界,以上場景的邊界,則是找不到節(jié)點(diǎn)(節(jié)點(diǎn)為null)的時(shí)候,就退出。
通過輸出的結(jié)果可以得知以下規(guī)律:
- 前序遍歷:中左右
- 中序遍歷:左中右
- 后序遍歷:左右中
而實(shí)現(xiàn)該規(guī)律的主要依據(jù),是通過遞歸的回溯導(dǎo)致,我們以中序遍歷為例子:
dfs(root.left) res.push(root.val) dfs(root.right)
當(dāng)?shù)谝粋€(gè)dfs(root.left)遞歸結(jié)束后,就會彈出'1'的節(jié)點(diǎn),然后就進(jìn)了dfs(root.right)的節(jié)點(diǎn),發(fā)現(xiàn)是個(gè)null,說明這個(gè)dfs(root.right)遞歸結(jié)束,那么此時(shí)則回到了'4'的節(jié)點(diǎn),然后就進(jìn)入了dfs(root.right)節(jié)點(diǎn)...
實(shí)際業(yè)務(wù)
二叉樹的遍歷,其實(shí)類比于我們常見操作菜單樹,或著樹形結(jié)構(gòu)的操作...
let tree = [ { id: '1', title: '節(jié)點(diǎn)1', children: [ { id: '1-1', title: '節(jié)點(diǎn)1-1' }, { id: '1-2', title: '節(jié)點(diǎn)1-2' } ] }, { id: '2', title: '節(jié)點(diǎn)2', children: [ { id: '2-1', title: '節(jié)點(diǎn)2-1' } ] } ]
當(dāng)我們要尋找遍歷每個(gè)節(jié)點(diǎn)的時(shí)候,同樣需要注意邊界,當(dāng)我們操作的數(shù)據(jù)沒有的時(shí)候或者不存在的時(shí)候,則退出當(dāng)次遍歷。
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) // ['節(jié)點(diǎn)1-1', '節(jié)點(diǎn)1-2', '節(jié)點(diǎn)2-1'] } } } dfs(tree) return res }
可能有人會有疑問,這也沒有利用到回溯的操作啊,那么我就換個(gè)場景,假如我給個(gè)你節(jié)點(diǎn)的id,你幫我找出他所有的父節(jié)點(diǎn),那么你可能會怎么操作呢?
const tree = [ { id: '1', title: '節(jié)點(diǎn)1', children: [ { id: '1-1', title: '節(jié)點(diǎn)1-1' }, { id: '1-2', title: '節(jié)點(diǎn)1-2' } ] }, { id: '2', title: '節(jié)點(diǎn)2', children: [ { id: '2-1', title: '節(jié)點(diǎn)2-1', children: [ { id: '2-1-1', title: '節(jié)點(diǎn)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]
其實(shí)以上核心的代碼為path.pop(),為什么需要這句代碼呢?我們可以通過leetcode上的排列組合問題來進(jìn)行討論。
組合問題
經(jīng)典的組合問題
以上面題目為例子,從1-4(n)的數(shù)字中,排列2(k)個(gè)數(shù)的組合。解這個(gè)題目,可以使用暴力的做法,嵌套for循環(huán)來完成該功能。
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]
細(xì)心朋友就會發(fā)現(xiàn),它嵌套for次數(shù)則是等于它排列(k)的次數(shù),那么我假如k的次數(shù)是10,或者20,那么豈不是要嵌套10個(gè)或者20個(gè)for循環(huán)。這套代碼寫下來,估計(jì)是個(gè)人都會暈了。在以上代碼塊中也可以發(fā)現(xiàn)重復(fù)的子問題也就是for循環(huán),它想要的結(jié)果則為當(dāng)我們找個(gè)了k個(gè)數(shù)的時(shí)候就停止。那么我們可以嘗試通過遞歸來解決該問題(遞歸for循環(huán)),但是這樣的過程還是很抽象的,需要借助圖例理解。(任何的組合問題,都可以理解成為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 }
當(dāng)我們選擇到了[1,2]之后,則需要回到1的位置,因?yàn)檫@個(gè)時(shí)候需要選擇3選項(xiàng),形成[1,3],那么回到'1'的操作,就類似于二叉樹遍歷回到父節(jié)點(diǎn)的操作,如果此時(shí)我們不操作,path.pop(),那么此時(shí)就會形成了[1,2,3],這樣的結(jié)果明顯不是我們想要,所以在操作push "3"的過程,需要先把2給pop掉。而遞歸的終止條件則為當(dāng)路徑的長度等于k的時(shí)候則退出。 另外在函數(shù)體中,還發(fā)現(xiàn)了startIndex的存在,這個(gè)是作為橫向for循環(huán)開始的位置,我們結(jié)合上面兩個(gè)for循環(huán)的代碼,是不是發(fā)現(xiàn)了j = i + 1的操作,而這個(gè)startIndex則是還原了這個(gè)操作而已。
到此這篇關(guān)于JavaScript中關(guān)于遞歸與回溯的實(shí)例詳解的文章就介紹到這了,更多相關(guān)JavaScript遞歸 回溯內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
網(wǎng)站基于flash實(shí)現(xiàn)的Banner圖切換效果代碼
這篇文章主要介紹了網(wǎng)站基于flash實(shí)現(xiàn)的Banner圖切換效果代碼,是基于Flash與js實(shí)現(xiàn)的banner圖片自動定時(shí)切換特效,并附有完整的示例源碼,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2014-10-10layui問題之模擬select點(diǎn)擊事件的實(shí)例講解
今天小編就為大家分享一篇layui問題之模擬select點(diǎn)擊事件的實(shí)例講解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08動態(tài)顯示可輸入的字?jǐn)?shù)提示還可以輸入的字?jǐn)?shù)
這篇文章主要介紹了動態(tài)顯示可輸入的字?jǐn)?shù)提示還可以輸入的字?jǐn)?shù),需要的朋友可以參考下2014-04-04JS控制頁面跳轉(zhuǎn)時(shí)未請求要跳轉(zhuǎn)的地址怎么回事
在js中通過window.location.href控制頁面跳轉(zhuǎn)時(shí),有時(shí)會跳轉(zhuǎn)至緩存頁面,并沒有真正去請求要跳轉(zhuǎn)的地址,導(dǎo)致頁面數(shù)據(jù)未能及時(shí)加載刷新。怎么解決呢?下面小編給大家解答下2016-10-10JavaScript 新手24條實(shí)用建議[TUTS+]
本文列出了24條能讓你的代碼編寫過程更為輕松高效的建議。也許您還是JavaScript初學(xué)者,剛剛寫完自己的Hello World,那這里有很多對您的工作將十分有用的小貼士;也許有些技巧您已經(jīng)知道,那就試試快速瀏覽一下,看能不能發(fā)現(xiàn)一點(diǎn)新東西吧!2009-06-06js實(shí)現(xiàn)創(chuàng)建刪除html元素小結(jié)
本文給大家簡單介紹了下使用javascript和jquery實(shí)現(xiàn)創(chuàng)建刪除html元素的方法,非常的實(shí)用,有需要的小伙伴可以參考下。2015-09-09livereload工具實(shí)現(xiàn)前端可視化開發(fā)【推薦】
本文將介紹一個(gè)工具--livereload。這是一款能根據(jù)你本地文件(html、css、js)的變化,自動跟蹤刷新瀏覽器的實(shí)時(shí)刷新工具,有了這個(gè)工具,會大大減輕你刷新頁面的工作量。下面跟著小編一起來看下吧2016-12-12JS繼承與閉包及JS實(shí)現(xiàn)繼承的三種方式
大家都知道,面向?qū)ο蟮娜筇卣鳌庋b、繼承、多態(tài)。下面通過本文給大家介紹JS繼承與閉包及JS實(shí)現(xiàn)繼承的三種方式,感興趣的朋友一起看看吧2017-10-10