了解JavaScript递归和调用堆栈的深度优先遍历
我有一个完美平衡的二叉树:了解JavaScript递归和调用堆栈的深度优先遍历,javascript,recursion,binary-tree,depth-first-search,Javascript,Recursion,Binary Tree,Depth First Search,我有一个完美平衡的二叉树: 1 / \ 2 5 / \ / \ 3 4 6 7 具有DF遍历的简单节点结构: function TreeNode(val){ this.val = val this.left = this.right = null this.addLeft = function(node){ this.left = node } this.addRight
1
/ \
2 5
/ \ / \
3 4 6 7
具有DF遍历的简单节点结构:
function TreeNode(val){
this.val = val
this.left = this.right = null
this.addLeft = function(node){
this.left = node
}
this.addRight = function(node){
this.right = node
}
}
function visit(node){
console.log(node.val)
}
function traverse(node){
if (node == null) return
visit(node)
if(node.left){
traverse(node.left)
}
if(node.right){
traverse(node.right)
}
}
我创建节点以表示树结构:
let rootNode = new TreeNode(1)
let node_A = new TreeNode(2)
let node_B = new TreeNode(5)
let node_C = new TreeNode(3)
let node_D = new TreeNode(4)
let node_E = new TreeNode(6)
let node_F = new TreeNode(7)
rootNode.addLeft(node_A)
rootNode.addRight(node_B)
node_A.addLeft(node_C)
node_A.addRight(node_D)
node_B.addLeft(node_E)
node_B.addRight(node_F)
调用遍历(rootNode)
正确打印出:
1
2
3
4
5
6
7
我理解递归是如何工作的,但仍然有点困惑JavaScript如何在调用堆栈中处理它<代码>遍历(rootNode)首先放在调用堆栈中,然后它到达if条件,在这里它检查rootNode
是否还有一个节点,因此它沿着路径继续,直到到达最后一个节点,即树节点(3)
。调用堆栈如下所示:
| |
| |
| traverse(TreeNode(3)) |
| traverse(TreeNode(2)) |
| traverse(rootNode) |
|_____________________________| |
TreeNode(3)
没有任何节点。left
或节点。right
因此它返回到if条件,并向下检查第二个条件节点。right
。然后它确实看到TreeNode(2)
有一个节点。右键遍历到TreeNode(4)
,并将其推到堆栈中
现在这部分让我困惑。当遍历(TreeNode(4)
完成时,JavaScript如何跟踪调用rootNode.right
?换句话说,它如何知道在左分支的末尾切换到右分支?因为输出将1
打印到7
,所以调用堆栈必须:
| |
| traverse(TreeNode(7)) |
| traverse(TreeNode(6)) |
| traverse(TreeNode(5)) |
| traverse(TreeNode(4)) |
| traverse(TreeNode(3)) |
| traverse(TreeNode(2)) |
| traverse(rootNode) |
|_____________________________|
但是我相信堆栈的顶部将首先弹出并返回,因此输出应该从7
开始,而不是从1
开始。所以我的第二个问题是,为什么控制台日志会正确地打印出从1
到7
的结果呢,您的解释遗漏了一些重要步骤,我认为这是正确的允许你得出一些无效的结论。特别是,堆栈看起来从来不像
traverse(TreeNode(7))
traverse(TreeNode(6))
traverse(TreeNode(5))
traverse(TreeNode(4))
traverse(TreeNode(3))
traverse(TreeNode(2))
traverse(rootNode)
作为记录,这些都不是特定于javascript的;因此我认为这实际上是关于提高您对递归的理解程度
让我们详细介绍一下遍历(rootNode)
调用
0: traverse ( node = rootNode ) {
| => if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ }
在这里,我使用的符号使一些事情更加明确:
- 我在调用之前放了一个数字;例如,通过这种方式,我可以将上面的调用称为“堆栈上的调用0”
- 我显示每个调用的参数分配
- 我显示了每个调用后面的函数代码,用一个箭头
=>
指示此特定调用代码的当前进度。这里我们将执行第一条指令
按照您似乎正在使用的约定,最近推送的项目(pop旁边)将显示在顶部
既然node
是rootNode
,而不是null
,它不会立即返回。在任何递归开始之前,它调用rootNode
上的visit()(并打印它们的值)在任何递归之前,所以它们在您遍历时打印,在您第一次到达每个节点时打印
然后它检查left
并找到一个truthy值(在本例中,是另一个节点对象),因此它调用遍历(node.left)
,最后得到一个类似
1: traverse ( node = node_A )
| => if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ }
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
因此,我们再次从一开始就使用新的节点值执行遍历()
,该值不为空,因此我们继续执行访问(节点)
,因为节点是节点A
,所以打印2。然后它检查左侧,这是真实的(节点C
),我们得到
2: traverse ( node = node_C )
| => if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ }
1: traverse ( node = node_A ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
2: traverse ( node = node_D )
| => if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ }
1: traverse ( node = node_A ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ => }
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
node_C
不是空的,所以它得到了visit()
ed,这将打印3
。现在我们检查left
,但是它是false(未定义的
)。所以我们检查right
,它也是false(未定义的
)。所以我们返回,现在堆栈显示
1: traverse ( node = node_A ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
现在我们已经完成了堆栈上调用1的travel()
代码的一半,所以我们从停止的地方(在=>
)开始。现在我们检查右侧的,这是真实的,我们得到了
2: traverse ( node = node_C )
| => if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ }
1: traverse ( node = node_A ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
2: traverse ( node = node_D )
| => if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ }
1: traverse ( node = node_A ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ => }
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
现在visit
将打印4
(因为node
是node\u D
,并且左和右都是错误的,所以我们返回到
1: traverse ( node = node_A ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| }
| if(node.right){
| traverse(node.right)
|_ => }
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
但是堆栈上的调用1已到达其代码的末尾,因此它返回,我们转到
0: traverse ( node = rootNode ) {
| if (node == null) return
| visit(node)
|
| if(node.left){
| traverse(node.left)
| => }
| if(node.right){
| traverse(node.right)
|_ }
呼叫0在它停止的地方开始,这意味着它将要右键检查,
(我认为这回答了您最初的问题)。执行沿着右分支进行,与沿着左分支进行完全相同,堆栈在不同时间包含对rootNode,node_B
的调用,然后rootNode,node_B,node_E
,然后再次执行rootNode,node_B
(但代码部分执行),然后是rootNode,node_B,node_F
,最后一次是rootNode,node_B
(代码即将完成),然后返回到rootNode
,然后对traverse
的初始调用最终返回。所有调用都与在该时间点传递到方法中的节点相关。该方法不知道任何其他状态。它知道该输入及其来源的方法。当节点4完成时,它退出到节点2,因为这是who调用了它,然后它又回到节点1,因为是节点1调用了它。然后它开始检查节点1的右侧节点。如果添加console.log('我已返回到节点'+node.val),您可以直观地看到这一点;
在每次遍历方法调用之后立即执行。Javascript首先按照到达函数调用的顺序提示所有函数调用,然后按照向后的顺序计算它们,有点像挖一个洞,然后再把它填满。因此,查看树的图表,从1到2,再到3和4,再到原来的功能会转移