使用遞迴遍歷並轉換樹形資料(以 TypeScript 為例)
一個朋友問我應該怎麼從一個樹的 JSON 陣列生成 HTML,使用 <ul>
和 <li>
來構建頁面元素。於是我簡單的畫了個樹型結構圖
然後寫了對應的模擬資料(JavaScript 物件)
const data = { name: "A", nodes: [ { name: "B", nodes: [{ name: "F" }] }, { name: "C" }, { name: "D", nodes: [ { name: "G" }, { name: "H" }, { name: "I", nodes: [{ name: "J" }, { name: "K" }] } ] }, { name: "E" } ]};
最後寫了一個遞迴,生成了 HTML 的樹型結構。原本是用 JavaScript ES6 寫的,為了表明資料結構,這裡改用 TypeScript 來寫:
interface INode { name: string; nodes?: INode[];}function makeTree(roots: INode[]): JQuery<HTMLElement> { function makeNode(node: INode): JQuery<HTMLElement> { const $div = $("<div>").text(node.name || ""); const $li = $("<li>").append($div); if (node.nodes && node.nodes.length) { $li.append(makeNodeList(node.nodes)); } return $li; } function makeNodeList(nodes: INode[]): JQuery<HTMLElement> { return nodes .map(child => makeNode(child)) .reduce(($ul, $li) => { return $ul.append($li); }, $("<ul>")); } return makeNodeList(roots);}
效果還是蠻不錯的
看看原始碼(轉譯成 JS 之後的):
然後朋友說沒看明白,好吧,那我從頭講起
遍歷方法
樹形資料的遍歷有兩種方法,大家都知道:廣度遍歷和深度遍歷。一般情況下,廣度遍歷是採用佇列來實現,而深度遍歷剛更適合使用遞迴來實現。
廣度遍歷
從圖上大致可以理解廣度遍歷的過程:
準備一個空佇列;
將根(單根或多根均可)節點放到佇列中;
從佇列中取出一個節點
處理(比如列印)這個節點
檢查節點的子節點,如果有,全部依次新增到佇列中
回到第 3 步開始處理,直到佇列為空(處理完成)
function travelWidely(roots: INode[]) { const queue: INode[] = [...roots]; while (queue.length) { const node = queue.shift()!; // 列印節點名稱及其子節點數 console.log(`${node.name} ${node.nodes && node.nodes.length || ""}`); if (node.nodes && node.nodes.length) { queue.push(...node.nodes); } }}// 開始遍歷travelWidely([data]);
const node = queue.shift()!
,這後面的!
字尾表示宣告其結果不為undefined
或null
。這是一個 TypeScript 語法。由於.shift()
在陣列中沒有元素時會返回undefined
,所以其返回型別被宣告為INode | undefined
,由於從邏輯可以保證.shift()
一定會返回一個節點物件,所以這裡用!
字尾忽略型別中的undefined
部分,使node
的型別被推導為INode
。
程式碼裡稍難理解一點的是要注意 queue
的內容和長度隨時在變化。如果想使用 for
代替 while
迴圈,節點序號會因 .shift()
而不斷變化,所以 i < queue.length
這樣的判斷是錯誤的。
深度遍歷
深度遍歷是一個遞迴過程,遞迴一直是程式設計的難點。
遞迴是一個迴圈往復的處理過程,它有兩個點需要注意:
遞迴呼叫點,遞迴呼叫自己(或另一個可能會呼叫自己的函式)
遞迴結束點,退出當前函式
以樹節點為例,我們期望處理過程是處理(列印)一個樹結點,即 printNode(node: INode)
。那麼它的
遞迴呼叫點:如果該節點有子節點,依次對子節點呼叫
printNode(children[i])
遞迴結束點:處理完所有子節點(子節點數量是有限的,所以一定會結束)
用一段虛擬碼描述這一過程
function printNode(node: INode) { // 處理該節點 console.log(node.name); // 遞迴呼叫點:迴圈對子節點呼叫 printNode node.nodes!.forEach(child => printNode(child)); // 遞迴結束點:迴圈完成,return}
上面兩句程式碼就完成了遞迴過程,但實際上情況還要複雜些,因為要處理入口和容錯。
// 注意引數支援傳入單根或多根,// 如果像 travelWidely 那樣只支援多根(單根是特例)也是可以的function travelDeeply(roots: INode | INode[]) { function printNode(node: INode) { console.log(`${node.name} ${node.nodes && node.nodes.length || ""}`); if (node.nodes && node.nodes.length) { // 依次對子節點遞迴呼叫 printNode node.nodes.forEach(child => printNode(child)); } } // 這裡 printNode 和 node => printNode(node) 等價 (Array.isArray(roots) ? roots : [roots]).forEach(printNode);}// 開始遍歷travelDeeply(data);
關於遞迴,我正好在慕課網上講生成資料解決方案的時候講到了,有興趣可以看看。
遍歷還沒講完
上面兩種遍歷都講到了,但是還沒講完——因為兩種遍歷都是以列印為例,而我們的目的是要生成 DOM 樹。生成 DOM 樹與純列印資訊的不同之處在於,我們不僅要使用節點資訊,還要從節點資訊生成 DOM 返回出來。
深度遍歷生成節點
這次先講深度遍歷,因為遞迴更容易實現。遞迴本身具有層次資訊,每進入一個遞迴呼叫點,就會深入一層,每離開一個遞迴結束點,就會減少一層。所以這個演算法本身能夠保留結構資訊,相應程式碼也會更容易實現。而且在本文一開始,就已經實現出來了。
需要注意的一點是那段程式碼用了兩個函式來完成遞迴過程:
makeNode
處理單個節點,它呼叫makeNodeList
處理子節點列表makeNodeList
遍歷節點列表,分別對其呼叫makeNode
來進行處理
makeNode
和 makeNodeList
的相互呼叫形成了遞迴,上述兩條都是遞迴呼叫點,而遞迴結束點同樣也有兩條:
makeNode
處理的節點沒有子節點時,不會呼叫makeNodeList
makeNodeList
中的迴圈結束時,不會再呼叫makeNode
廣度遍歷生成節點
廣度遍歷的過程是把所有節點扁平化到一個佇列中了,這個過程是不可逆 的,換句話說,我們在處理過程中丟掉了樹形結構資訊。然後我們要生成的 DOM 樹,是需要結構資訊的——因此,需要將結構資訊附加在每個節點上。這裡我們把生成的 DOM 和資料節點繫結起來,由 DOM 儲存結構資訊。為此,需要修改一下節點型別
interface INode { name: string; nodes?: INode[]; dom: JQuery; // 附加生成的 DOM}
function makeTreeWidely(roots: INode[]): JQuery { // 從一組節點生成 <ul>,為每個節點生成並附加 <li>, // 同時將 <li> 到到 <ul> 中儲存結構資訊 function makeUl(nodes: INode[]) { return nodes .map(node => { const $li = $("<li>") .append($("<div>").text(node.name || "")); node.dom = $li; return $li; }) .reduce(($ul, $li) => $ul.append($li), $("<ul>")); } const $rootUl = makeUl(roots); const queue: INode[] = [...roots]; while (queue.length) { const node = queue.shift()!; if (node.nodes && node.nodes.length) { const $ul = makeUl(node.nodes); node.dom.append($ul); queue.push(...node.nodes); } } return $rootUl;}
雖然這裡和上面講遞迴遍歷 printNode
的時候一樣定義了區域性函式表示式 makeUl
,但這裡沒有遞迴,因為 makeUl
內部沒有呼叫自身,或者某個會呼叫 makeUl
的函式。
但問題還是再深入一點,因為上面的程式碼改變了原資料。而一般情況下,我們應該儘量避免這樣的副作用
沒有副作用的廣度遍歷生成節點
// 宣告一個新結構,它把 INode 和 DOM 組合在一起。// 這個結構將代替 INode 作為佇列的元素型別interface IDomNode { node: INode; dom: JQuery;}function makeTreeWidely(roots: INode[]): JQuery { // convert 將節點陣列轉換為 IDomNode 陣列, // 同時還幹了原來 makeUl 乾的事情,返回一個 $ul function convert(nodes: INode[]) { const domNodes = nodes .map(node => { const $li = $("<li>") .append($("<div>").text(node.name || "")); return { node, dom: $li }; }); const $ul = domNodes .reduce(($ul, dn) => $ul.append(dn.dom), $("<ul>")); // 將兩個陣列組成一個元組(物件)返回 return { domNodes, $ul }; } // 解析元組,宣告變數 queue 和 $rootUl, // 並分別將 domNodes 和 $ul 的值賦值給 queue 和 $rootUl 兩個變數 const { domNodes: queue, $ul: $rootUl } = convert(roots); while (queue.length) { const { node, dom } = queue.shift()!; if (node.nodes && node.nodes.length) { const { domNodes, $ul } = convert(node.nodes); dom.append($ul); queue.push(...domNodes); } } return $rootUl;}
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4798/viewspace-2817615/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 遍歷二叉樹-------遞迴&非遞迴二叉樹遞迴
- 什麼是遍歷二叉樹,JavaScript實現二叉樹的遍歷(遞迴,非遞迴)二叉樹JavaScript遞迴
- 二叉樹的遍歷 → 不用遞迴,還能遍歷嗎二叉樹遞迴
- 二叉樹的四種遍歷(遞迴與非遞迴)二叉樹遞迴
- 資料結構-樹以及深度、廣度優先遍歷(遞迴和非遞迴,python實現)資料結構遞迴Python
- 樹3-二叉樹非遞迴遍歷(棧)二叉樹遞迴
- 非遞迴先序遍歷二叉樹遞迴二叉樹
- Day14 | 二叉樹遞迴遍歷二叉樹遞迴
- 遍歷二叉樹的遞迴與非遞迴程式碼實現二叉樹遞迴
- 遞迴遍歷樹狀結構優雅實現遞迴
- 二叉樹的非遞迴遍歷寫法二叉樹遞迴
- 遍歷二叉樹的迭代和遞迴方法二叉樹遞迴
- 【Java資料結構與演算法筆記(二)】樹的四種遍歷方式(遞迴&非遞迴)Java資料結構演算法筆記遞迴
- 二叉樹建立後,如何使用遞迴和棧遍歷二叉樹?二叉樹遞迴
- 二叉樹的前中後序遍歷(遞迴和非遞迴版本)二叉樹遞迴
- python實現二叉樹及其七種遍歷方式(遞迴+非遞迴)Python二叉樹遞迴
- 二叉樹——後序遍歷的遞迴與非遞迴演算法二叉樹遞迴演算法
- Java遞迴遍歷資料夾及檔案過濾器使用(FileFilter)Java遞迴過濾器Filter
- 二叉樹的建立與遍歷(遞迴實現)二叉樹遞迴
- 二叉樹的所有遍歷非遞迴實現二叉樹遞迴
- js遞迴遍歷講解JS遞迴
- [資料結構]二叉樹的前中後序遍歷(遞迴+迭代實現)資料結構二叉樹遞迴
- 遞迴遍歷網站所有 url遞迴網站
- 遞迴函式-樹形列表遞迴函式
- 非遞迴實現先序遍歷和中序遍歷遞迴
- 迴圈遍歷二叉樹二叉樹
- 高效遍歷匹配Json資料,避免巢狀迴圈[轉]JSON巢狀
- 遞迴遍歷物件獲取value值遞迴物件
- 二叉樹的建立、遍歷、廣義錶轉換二叉樹
- 資料結構初階--二叉樹(前中後序遍歷遞迴+非遞迴實現+相關求算結點實現)資料結構二叉樹遞迴
- 程式碼隨想錄演算法訓練營,9月9日 | 二叉樹遞迴遍歷,迭代遍歷,層序遍歷演算法二叉樹遞迴
- [work] python巢狀字典的遞迴遍歷Python巢狀遞迴
- 迭代及用遞迴遍歷File檔案遞迴
- 遞迴樹形查詢所有分類遞迴
- Android遍歷所有控制元件的遞迴和非遞迴實現Android控制元件遞迴
- 玩轉二叉樹(樹的遍歷)二叉樹
- 刷題系列 - Python用非遞迴實現二叉樹前序遍歷Python遞迴二叉樹
- Vue遞迴元件+Vuex開發樹形元件Tree--遞迴元件Vue遞迴元件