翻譯:瘋狂的技術宅
英文:https://code.tutsplus.com/art...
說明:本文翻譯自系列文章《Data Structures With JavaScript》,總共爲四篇,原做者是在美國硅谷工做的工程師 Cho S. Kim。這是本系列的第四篇。javascript
說明:本專欄文章首發於公衆號:jingchengyideng 。html
樹是 web 開發中最經常使用的數據結構之一。 這種說法對開發者和用戶都是正確的。每一個編寫HTML的開發者,只要把網頁載入瀏覽器就會建立一個樹,樹一般被稱爲文檔對象模型(DOM)。相應地,每一個在互聯網上瀏覽信息的人,也都是以DOM樹的形式接受信息。 每一個編寫HTML而且將其加載到Web瀏覽器的Web開發人員都建立了一個樹,這被稱爲文檔對象模型(DOM)。互聯網上的全部用戶,在獲取信息時,都是以樹的形式收——即DOM。 java
如今,高潮來了:你正在讀的本文在瀏覽器中就是以樹的形式進行渲染的。文字由<p>
元素進行表示;<p>
元素又嵌套在<body>
元素中;<body>
元素又嵌套在<html>
元素中。 您正在閱讀的段落表示爲<p>
元素中的文本;<p>
元素嵌套在<body>
元素中;<body>
元素嵌套在<html>
元素中。node
這些嵌套數據和家族數相似。 <heml>
是父元素,<body>
是子元素,<p>
又是<body>
的子元素 若是這個比喻對你有點用的話,你將會發如今咱們介紹樹的時候會用到更多的類比。web
在本文中,咱們將會經過兩種不一樣的遍歷方式來建立一個樹:深度優先(DFS)和廣度優先(BFS)。 (若是你對遍歷這個詞感到比較陌生,不妨將他想象成訪問樹中的每個節點。) 這兩種類型的遍歷強調了與樹交互的不一樣方式, DFS和BFS分別用棧和隊列來訪問節點。 這聽起來很酷!瀏覽器
在計算機科學中,樹是一種用節點來模擬分層數據的數據結構。每一個樹節點都包含他自己的數據及指向其餘節點的指針。數據結構
節點和指針這些術語可能對一些讀者來講比較陌生,因此讓咱們用類比來進一步描述他們。 讓咱們將樹與組織圖結構圖進行比較。 這個結構圖有一個頂級位置(根節點),好比CEO。 在這個節點下面還有一些其餘的節點,好比副總裁(VP)。app
爲了表示這種關係,咱們用箭頭從CEO指向VP。 一個位置,好比CEO,是一個節點;咱們建立的CEO到VP的關係是一個指針。 在咱們的組織結構圖中去建立更多的關係,咱們只要重複這些步驟便可---咱們讓一個節點指向另外一個節點。ide
在概念層次上,我但願節點和指針有意義。 在實際中,咱們能從更科學的實例中獲取收益。 讓咱們來思考DOM。 DOM有<html>
元素做爲其頂級位置(根節點)。 這個節點指向<head>
元素和<body>
元素。 這些步驟在DOM的全部節點中重複。函數
這種設計的一個優勢是可以嵌套節點:例如:一個<ul>
元素可以包含不少個<li>
元素;此外,每一個<li>
元素能擁有兄弟<li>
元素。這很怪異,可是確實真實有趣!
因爲每一個樹都包含節點,其能夠是來自樹的單獨構造器,咱們將概述兩個構造函數的操做:Node
和Tree
data
存儲值。
parent
指向節點的父節點。
children
指向列表中的下一個節點。
_root
指向一個樹的根節點。
traverseDF(callback)
對樹進行DFS遍歷。
traverseBF(callback)
對樹進行BFS遍歷。
contains(data, traversal)
搜索樹中的節點。
add(data, toData, traverse)
向樹中添加節點。
remove(child, parent)
移除樹中的節點。
如今開始寫樹的代碼!
在實現中,咱們首先定義一個叫作Node
的函數,而後構造一個Tree
。
function Node(data) { this.data = data; this.parent = null; this.children = []; }
每個Node
的實例都包含三個屬性:data
,parant
,和children
。 第一個屬性保存與節點相關聯的數據。 第二個屬性指向一個節點。 第三個屬性指向許多子節點。
如今讓咱們來定義Tree
的構造函數,其中包括Node構造函數的定義:
function Tree(data) { var node = new Node(data); this._root = node; }
Tree
包含兩行代碼。 第一行建立了一個Node
的新實例;第二行讓node等於樹的根節點。
Tree
和Node
的定義只須要幾行代碼。 可是,經過這幾行足以幫助咱們模擬分層數據。 爲了證實這一點,讓咱們用一些示例數據去建立Tree的示例(和間接的Node
)。
var tree = new Tree('CEO'); // {data: 'CEO', parent: null, children: []} tree._root;
幸虧有parent
和children
的存在,咱們能夠爲_root
添加子節點和讓這些子節點的父節點等於_root
。 換一種說法,咱們能夠模擬分層數據的建立。
接下來咱們將要建立如下五種方法。
traverseDF(callback)
traverseBF(callback)
contains(data, traversal)
add(child, parent)
remove(node, parent)
由於每種方法都須要遍歷一個樹,因此咱們首先要實現一個方法去定義不一樣的樹遍歷。 (遍歷樹是訪問樹的每一個節點的正式方式。)
traverseDF(callback)
這種方法以深度優先方式遍歷樹。
Tree.prototype.traverseDF = function(callback) { // this is a recurse and immediately-invoking function (function recurse(currentNode) { // step 2 for (var i = 0, length = currentNode.children.length; i < length; i++) { // step 3 recurse(currentNode.children[i]); } // step 4 callback(currentNode); // step 1 })(this._root); };
traverseDF(callback)
有一個參數callback
。 若是對這個名字不明白,callback
被假定是一個函數,將在後面被traverseDF(callback)
調用。
traverseDF(callback)
的函數體含有另外一個叫作recurse
的函數。 這個函數是一個遞歸函數! 換句話說,它是自我調用和自我終止。 使用recurse
的註釋中提到的步驟,我將描述遞歸用來recurse
整個樹的通常過程。
這裏是步驟:
當即使用樹的根節點做爲其參數調用recurse
。 此時,currentNode
指向當前節點。
進入for
循環而且從第一個子節點開始,每個子節點都迭代一次currentNode
函數。
在for
循環體內,使用currentNode
的子元素調用遞歸。 確切的子節點取決於當前for
循環的當前迭代。
當currentNode
不存在子節點時,咱們退出for
循環並callback
咱們在調用traverseDF(callback)
期間傳遞的回調。
步驟2(自終止),3(自調用)和4(回調)重複,直到咱們遍歷樹的每一個節點。
遞歸是一個很是困難的話題,須要一個完整的文章來充分解釋它。因爲遞歸的解釋不是本文的重點 —— 重點是實現一棵樹 —— 我建議任何讀者沒有很好地掌握遞歸作如下兩件事。
首先,實驗咱們當前的traverseDF(callback)
實現,並嘗試必定程度上理解它是如何工做的。 第二,若是你想要我寫一篇關於遞歸的文章,那麼請在本文的評論中請求它。
如下示例演示如何使用traverseDF(callback)
遍歷樹。要遍歷樹,我將在下面的示例中建立一個。我如今使用的方法不是罪理想的,但它能很好的工做。 一個更好的方法是使用add(value)
,咱們將在第4步和第5步中實現。
var tree = new Tree('one'); tree._root.children.push(new Node('two')); tree._root.children[0].parent = tree; tree._root.children.push(new Node('three')); tree._root.children[1].parent = tree; tree._root.children.push(new Node('four')); tree._root.children[2].parent = tree; tree._root.children[0].children.push(new Node('five')); tree._root.children[0].children[0].parent = tree._root.children[0]; tree._root.children[0].children.push(new Node('six')); tree._root.children[0].children[1].parent = tree._root.children[0]; tree._root.children[2].children.push(new Node('seven')); tree._root.children[2].children[0].parent = tree._root.children[2]; /* creates this tree one ├── two │ ├── five │ └── six ├── three └── four └── seven */
如今,讓咱們調用traverseDF(callback)
。
tree.traverseDF(function(node) { console.log(node.data) }); /* logs the following strings to the console 'five' 'six' 'two' 'three' 'seven' 'four' 'one' */
traverseBF(callback)
這個方法使用深度優先搜索去遍歷樹
深度優先搜索和廣度優先搜索之間的差異涉及樹的節點訪問的序列。 爲了說明這一點,讓咱們使用traverseDF(callback)
建立的樹。
/* tree one (depth: 0) ├── two (depth: 1) │ ├── five (depth: 2) │ └── six (depth: 2) ├── three (depth: 1) └── four (depth: 1) └── seven (depth: 2) */
如今,讓咱們傳遞traverseBF(callback)
和咱們用於traverseDF(callback)的回調。
tree.traverseBF(function(node) { console.log(node.data) }); /* logs the following strings to the console 'one' 'two' 'three' 'four' 'five' 'six' 'seven' */
來自控制檯的日誌和咱們的樹的圖顯示了關於廣度優先搜索的模式。從根節點開始;而後行進一個深度並訪問該深度從左到右的每一個節點。重複此過程,直到沒有更多的深度要移動。
因爲咱們有一個廣度優先搜索的概念模型,如今讓咱們實現使咱們的示例工做的代碼。
Tree.prototype.traverseBF = function(callback) { var queue = new Queue(); queue.enqueue(this._root); currentTree = queue.dequeue(); while(currentTree){ for (var i = 0, length = currentTree.children.length; i < length; i++) { queue.enqueue(currentTree.children[i]); } callback(currentTree); currentTree = queue.dequeue(); } };
咱們對traverseBF(callback)
的定義包含了不少邏輯。 所以,我會用下面的步驟解釋這些邏輯:
建立 Queue
的實例。
調用traverseBF(callback)
產生的節點添加到Queue
的實例。
定義一個變量currentNode
而且將其值初始化爲剛纔添加到隊列裏的node
當currentNode
指向一個節點時,執行wille
循環裏面的代碼。
用for
循環去迭代currentNode
的子節點。
在for
循環體內,將每一個子元素加入隊列。
獲取currentNode
並將其做爲callback
的參數傳遞。
將currentNode
從新分配給正從隊列中刪除的節點。
直到currentNode
再也不指向任何節點——也就是說樹中的每一個節點都訪問過了——重複4-8步。
contains(callback, traversal)
讓咱們定義一個方法,能夠在樹中搜索一個特定的值。去使用咱們建立的任意一種樹的遍歷方法,咱們已經定義了contains(callback, traversal)
接收兩個參數:搜索的數據和遍歷的類型。
Tree.prototype.contains = function(callback, traversal) { traversal.call(this, callback); };
在contains(callback, traversal)
函數體內,咱們用call
方法去傳遞this
和callback
。 第一個參數將traversal
綁定到被調用的樹contains(callback,traversal)
;第二個參數是在樹中每一個節點上調用的函數。
想象一下,咱們要將包含奇數數據的任何節點記錄到控制檯,並使用BFS遍歷樹中的每一個節點。 咱們能夠這麼寫代碼:
// tree is an example of a root node tree.contains(function(node){ if (node.data === 'two') { console.log(node); } }, tree.traverseBF); add(data, toData, traversal)
add(data, toData, traversal)
如今有了一個能夠搜索樹中特定節點的方法。 讓咱們定義一個容許向指定節點添加節點的方法。
Tree.prototype.add = function(data, toData, traversal) { var child = new Node(data), parent = null, callback = function(node) { if (node.data === toData) { parent = node; } }; this.contains(callback, traversal); if (parent) { parent.children.push(child); child.parent = parent; } else { throw new Error('Cannot add node to a non-existent parent.'); } };
add(data, toData, traversal)
定義了三個參數。 第一個參數data
用來建立一個Node
的新實例。 第二個參數toData
用來比較樹中的每一個節點。 第三個參數traversal
,是這個方法中用來遍歷樹的類型。
在add(data, toData, traversal)
函數體內,咱們聲明瞭三個變量。 第一個變量child
表明初始化的Node
實例。 第二個變量parent
初始化爲null
;可是未來會指向匹配toData
值的樹中的任意節點。parent
的從新分配發生在咱們聲明的第三個變量,這就是callback
。
callback
是一個將toData
和每個節點的data
屬性作比較的函數。 若是if
語句的值是true
,那麼parent
將被賦值給if
語句中匹配比較的節點。
每一個節點的toData
在contains(callback, traversal)
中進行比較。遍歷類型和callback
必須做爲contains(callback, traversal)
的參數進行傳遞。
最後,若是parent
不存在於樹中,咱們將child
推入parent.children
; 同時也要將parent
賦值給child
的父級。不然,將拋出錯誤。
讓咱們用add(data, toData, traversal)
作個例子:
var tree = new Tree('CEO'); tree.add('VP of Happiness', 'CEO', tree.traverseBF); /* our tree 'CEO' └── 'VP of Happiness' */
這裏是add(addData, toData, traversal)
的更加複雜的例子:
var tree = new Tree('CEO'); tree.add('VP of Happiness', 'CEO', tree.traverseBF); tree.add('VP of Finance', 'CEO', tree.traverseBF); tree.add('VP of Sadness', 'CEO', tree.traverseBF); tree.add('Director of Puppies', 'VP of Finance', tree.traverseBF); tree.add('Manager of Puppies', 'Director of Puppies', tree.traverseBF); /* tree 'CEO' ├── 'VP of Happiness' ├── 'VP of Finance' │ ├── 'Director of Puppies' │ └── 'Manager of Puppies' └── 'VP of Sadness' */
remove(data, fromData, traversal)
爲了完成Tree
的實現,咱們將添加一個叫作remove(data, fromData, traversal)
的方法。 跟從DOM裏面移除節點相似,這個方法將移除一個節點和他的全部子級。
Tree.prototype.remove = function(data, fromData, traversal) { var tree = this, parent = null, childToRemove = null, index; var callback = function(node) { if (node.data === fromData) { parent = node; } }; this.contains(callback, traversal); if (parent) { index = findIndex(parent.children, data); if (index === undefined) { throw new Error('Node to remove does not exist.'); } else { childToRemove = parent.children.splice(index, 1); } } else { throw new Error('Parent does not exist.'); } return childToRemove; };
與add(data, toData, traversal)
相似,移除將遍歷樹以查找包含第二個參數的節點,如今爲fromData
。 若是這個節點被發現了,那麼parent
將指向它。
在這時候,咱們到達了第一個if
語句。 若是parent不存在,將拋出錯誤。 若是parent
不存在,咱們使用parent.children
調用findIndex()
和咱們要從parent
節點的子節點中刪除的數據 (findIndex()
是一個幫助方法,我將在下面定義。)
function findIndex(arr, data) { var index; for (var i = 0; i < arr.length; i++) { if (arr[i].data === data) { index = i; } } return index; }
在findIndex()
裏面,如下邏輯將發生。 若是parent.children
中的任意一個節點包含匹配data
值的數據,那麼變量index
賦值爲一個整數。 若是沒有子級的數值屬性匹配data
,那麼index保留他的默認值undefined
。 在最後一行的findIndex()
方法,咱們返回一個index。
咱們如今去remove(data, fromData, traversal)
若是index
的值是undefined
,將會拋出錯誤。 若是index
的值存在,咱們用它來拼接咱們想從parent
的子節點中刪除的節點。一樣咱們給刪除的子級賦值爲childToRemove
。
最後,咱們返回childToRemove
。
到此爲止Tree
已經徹底實現。回過頭看看,咱們到底完成了多少工做:
function Node(data) { this.data = data; this.parent = null; this.children = []; } function Tree(data) { var node = new Node(data); this._root = node; } Tree.prototype.traverseDF = function(callback) { // this is a recurse and immediately-invoking function (function recurse(currentNode) { // step 2 for (var i = 0, length = currentNode.children.length; i < length; i++) { // step 3 recurse(currentNode.children[i]); } // step 4 callback(currentNode); // step 1 })(this._root); }; Tree.prototype.traverseBF = function(callback) { var queue = new Queue(); queue.enqueue(this._root); currentTree = queue.dequeue(); while(currentTree){ for (var i = 0, length = currentTree.children.length; i < length; i++) { queue.enqueue(currentTree.children[i]); } callback(currentTree); currentTree = queue.dequeue(); } }; Tree.prototype.contains = function(callback, traversal) { traversal.call(this, callback); }; Tree.prototype.add = function(data, toData, traversal) { var child = new Node(data), parent = null, callback = function(node) { if (node.data === toData) { parent = node; } }; this.contains(callback, traversal); if (parent) { parent.children.push(child); child.parent = parent; } else { throw new Error('Cannot add node to a non-existent parent.'); } }; Tree.prototype.remove = function(data, fromData, traversal) { var tree = this, parent = null, childToRemove = null, index; var callback = function(node) { if (node.data === fromData) { parent = node; } }; this.contains(callback, traversal); if (parent) { index = findIndex(parent.children, data); if (index === undefined) { throw new Error('Node to remove does not exist.'); } else { childToRemove = parent.children.splice(index, 1); } } else { throw new Error('Parent does not exist.'); } return childToRemove; }; function findIndex(arr, data) { var index; for (var i = 0; i < arr.length; i++) { if (arr[i].data === data) { index = i; } } return index; }
樹能夠用來模擬分層數據。咱們周圍有許多相似這種類型的層次結構,例如網頁和族譜。當你發現本身須要使用層次結構來結構化數據時,能夠考慮使用樹。
歡迎掃描二維碼關注公衆號,天天推送我翻譯的技術文章。