翻译:疯狂的技术宅
英文: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; }
树能够用来模拟分层数据。咱们周围有许多相似这种类型的层次结构,例如网页和族谱。当你发现本身须要使用层次结构来结构化数据时,能够考虑使用树。
欢迎扫描二维码关注公众号,天天推送我翻译的技术文章。