树基本上只是花哨的链表,并且在树上创建和删除节点非常简单。另一方面,当它们未排序时,搜索会有些棘手,因此我们将研究几种不同的方式来处理整个树的搜索。
先决条件
您将需要基本了解什么是树以及它们如何工作。我们使用Binary Search Tree的特定示例,但是与确切的实现相比,它们更多的是技术和模式,并且可以轻松地适用于任何类型的树。
概念
使用JS二叉搜索树我们可以使用相同的系统来创建一个新节点,就像找到一个一样。标准树(例如文件系统)不遵循任何特定规则,因此迫使我们通过树或子树查看每个项目以查找所需内容。这就是为什么搜索特定文件可能要花费这么长时间的原因。
没有很多方法可以优化过去,O(n)
但是有两种主要的“哲学”可以搜索整个树,即通过广度优先(水平在同级之间)或深度优先(垂直在父母与孩子之间)进行搜索。
树
由于二叉搜索树最容易建立,因此我们可以将仅添加节点的快速树放在一起。
class Node {
constructor(val) {
this.val = val;
this.right = null;
this.left = null;
};
};
class BST {
constructor() {
this.root = null;
};
create(val) {
const newNode = new Node(val);
if (!this.root) {
this.root = newNode;
return this;
};
let current = this.root;
const addSide = side => {
if (!current[side]) {
current[side] = newNode;
return this;
};
current = current[side];
};
while (true) {
if (val === current.val) return this;
if (val < current.val) addSide(‘left‘);
else addSide(‘right‘);
};
};
};
const tree = new BST();
tree.create(20);
tree.create(14);
tree.create(57);
tree.create(9);
tree.create(19);
tree.create(31);
tree.create(62);
tree.create(3);
tree.create(11);
tree.create(72);
广度优先搜索
广度优先搜索的特征在于,它着重于从左到右,在每个级别上的每个项目,然后再转移到下一个。
这包括三个主要部分:当前节点,访问的节点列表以及用于跟踪需要查看哪些节点的基本队列(我们将只使用数组,因为它将永远不会很长) 。
无论我们是什么人current
,我们都会将其子级(从左到右)推入队列,因此看起来像[20, 14, 57]
。然后,我们将切换current
到队列中的下一个项目,并将其左,右子级添加到队列的末尾[14, 57, 9, 19]
。
现在,visited
当我们移至下一个项目,查找其子项目并将其添加到队列中时,可以删除并添加当前项目。这将重复进行,直到我们的队列为空并且每个值都在中visited
。
BFS() {
let visited = [],
queue = [],
current = this.root;
queue.push(current);
while (queue.length) {
current = queue.shift();
visited.push(current.val);
if (current.left) queue.push(current.left);
if (current.right) queue.push(current.right);
};
return visited;
}
console.log(tree.BFS()); //[ 20, 14, 57, 9, 19, 31, 62, 3, 11, 72 ]
深度优先搜索
深度优先搜索与完成每个级别相比,更关心的是完成从树的整个侧面到叶子的遍历。
有处理这种方式主要有三种,preOrder
,postOrder
,和inOrder
但他们彼此只是很轻微的修改来改变输出顺序。更好的是,我们甚至不必担心队列。
从根开始,我们将使用一个简短的递归函数来记录我们的节点,然后尽可能向下移动到左边,并记录其路径。完成左侧操作后,它将开始处理剩余的右侧值,直到记录完整个树为止。最终访问应该看起来像[24, 14, 9, 3, 11, 19, ...]
。
preOrder() {
let visited = [],
current = this.root;
let traverse = node => {
visited.push(node.val);
if (node.left) traverse(node.left);
if (node.right) traverse(node.right);
};
traverse(current);
return visited;
}
console.log(tree.preOrder()); // [ 20, 14, 9, 3, 11, 19, 57, 31, 62, 72 ]
您可能已经猜到了,postOrder
与的相反preOrder
,我们仍在垂直工作,但是我们从底部到顶部进行搜索,而不是从根到叶。
我们将从最底部的节点开始,并记录其及其兄弟节点,然后再移至其父节点。被访问的前半部分应该看起来像这样[3, 11, 9, 19, 14, ...]
,因为它可以使它在树上冒泡。
我们可以通过visited
在两个遍历完成之后将节点推入来轻松实现此目的。
postOrder() {
let visited = [],
current = this.root;
let traverse = node => {
if (node.left) traverse(node.left);
if (node.right) traverse(node.right);
visited.push(node.val);
};
traverse(current);
return visited;
}
console.log(tree.postOrder()); // [ 3, 11, 9, 19, 14, 31, 72, 62, 57, 20 ]
与的访问类似postOrder
,preOrder
访问从下而上进行,但仅在访问任何同级之前访问父项。
在遍历左侧之后,在右侧之前,我们可以推入列表,而不是开始或结束。我们的结果将如下所示[3, 9, 11, 14, 19, 20, ...]
。
inOrder() {
let visited = [],
current = this.root;
let traverse = node => {
if (node.left) traverse(node.left);
visited.push(node.val);
if (node.right) traverse(node.right);
};
traverse(current);
return visited;
}
console.log(tree.inOrder()); // [ 3, 9, 11, 14, 19, 20, 31, 57, 62, 72 ]
总结思想
当然,所有这些算法都是O(n)
因为要着眼于每个节点,没有太多的捷径或技巧。
请记住,这些并不是需要记住的确切实现,而是解决问题和构建更有价值的算法的通用模式。一旦您了解了下划线的概念,便可以轻松地将它们适应任何语言或框架。
原文地址:https://blog.51cto.com/14763751/2484642