问题描述
我目前正在与树木一起学习,并且正在处理遍历的不同实现。
class Node {
constructor(data) {
this.data = data;
this.children = [];
}
add(data) {
this.children.push(new Node(data));
}
remove(data) {
this.children = this.children.filter(node => {
return node.data !== data;
})
}
}
traverseBF(fn) {
const arr = [this.root];
while (arr.length) {
const node = arr.shift();
arr.push(...node.children);
fn(node); //what role does this play?
}
return count;
}
traverseDF(fn) {
const arr = [this.root];
while (arr.length) {
const node = arr.shift();
arr.unshift(...node.children);
fn(node); //what role does this play???
}
}
我以为我已经了解到回调具有在其中声明的上下文并且能够访问外部函数中的变量,我认为这是arr保持最新状态且回调函数不可或缺的原因使BFS / DFS在此实例中工作。但是,学习计算宽度级别破坏了我的理解。
function levelWidth(root) {
const arr = [root,'s'];
const counters = [0];
while (arr.length > 1) {
const node = arr.shift();
if (node === 's') {
counters.push(0);
arr.push('s');
} else {
arr.push(...node.children);
counters[counters.length - 1]++;
}
}
return counters;
}
此BFS搜索尚无回调,因此遍历正常。谁能帮助我更好地理解为什么在一开始而不是在此情况下需要它?
当我这样调用遍历时到底发生了什么?
const letters = [];
const t = new Tree();
t.root = new Node('a');
t.root.add('b');
t.root.add('d');
t.root.children[0].add('c');
t.root.children[1].add('e');
t.traverseBF(node => {
letters.push(node.data);
});
console.log(letters);
解决方法
这里没有错误或正确的地方。
回调版本在两个方面有所不同:
-
使用访问的节点不应用任何逻辑。它仅照顾遍历,而不照顾任何其他逻辑。任何特定的逻辑都留给了调用方,调用方可以完全为此目的传递回调。在最后一个示例中,该特定逻辑包括将节点的
data
值收集到一个数组中。但是请注意,遍历函数不了解此逻辑,这是一个很好的关注点分离。注意:
return count
末尾的traverseBF(fn)
应该不存在(没有count
) -
在访问所有所有节点之前,呼叫者不会一直等待。
非回调版本不仅访问节点,而且还会在这些节点上进行特定于的处理(即进行计数),并且它仅返回该处理的结果。这不是通用的。如果您想要遍历一个完全不同的用途,则不能使用此功能,因为它实际上并不会告诉调用方它已访问的节点或发生的顺序。
您还可以想象一种“中间”遍历实现:一种不使用回调,而只是将所有访问的节点收集到一个数组中,然后按其顺序返回完整的节点数组的实现被参观了。这是更通用的方法,但是调用者必须 wait ,直到访问完所有节点,然后才能开始在返回的节点数组上应用自己的算法。
所以,我想说回调版本更加灵活和通用。
但是,实现这种通用遍历的更现代的方法不是通过回调系统,而是通过 generator 。
这是它的外观(请注意开头的*
)
* traverseBF() {
const arr = [this.root];
while (arr.length) {
const node = arr.shift();
arr.push(...node.children);
yield node; // <---
}
}
* traverseDF() {
const arr = [this.root];
while (arr.length) {
const node = arr.shift();
arr.unshift(...node.children);
yield node; // <---
}
}
调用方必须意识到这些方法是生成器这一事实,但是您可以使用如下所示的for循环:
let letters = [];
for (let node of t.traverseDF()) {
// do something with this node before continuing the traversal
letters.push(node.data);
}
console.log(letters);
此处的另一个优点是,调用方始终可以决定终止遍历。在上面的代码中,循环中较早的break
确实意味着遍历将不再完成。对于前面提到的所有其他方法,您必须触发一个异常才能使其成为可能。在所有其他情况下,遍历都必须运行到完成。