Warning: file_get_contents(/data/phpspider/zhask/data//catemap/9/javascript/414.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
了解JavaScript递归和调用堆栈的深度优先遍历_Javascript_Recursion_Binary Tree_Depth First Search - Fatal编程技术网

了解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,再到原来的功能会转移