跳转到内容

树遍历

来自维基教科书,开放的书籍,开放的世界


试卷 1 - ⇑ 算法基础 ⇑

← 图遍历 树遍历 逆波兰 →


一棵 是图的一种特殊情况,因此上一章的 图遍历 算法也适用于树。图遍历可以从任何节点开始,但在树的情况下,遍历总是从根节点开始。二叉树可以以三种额外的遍历方式进行遍历。以下树将作为重复出现的示例。

广度优先

[编辑 | 编辑源代码]

以广度优先顺序遍历树意味着,在访问节点 X 之后,将访问 X 的所有子节点,然后访问 X 的所有“孙子节点”(即子节点的子节点),然后访问 X 的所有“曾孙子节点”,等等。换句话说,树是通过扫描一层中的宽度来遍历的,然后再访问下一层,如动画所示

广度优先遍历的动画示例

节点的子节点可以以任何顺序访问,但请记住算法使用队列,因此如果节点 X 在节点 Y 之前入队(在动画中为灰色),则 X 的子节点将在 Y 的子节点之前被访问(在动画中为黑色)。对于本章开头的示例树,两种可能的广度优先遍历是 F B G A D I C E H 和 F G B I D A H E C。在第二次遍历中,G 在 B 之前被访问,所以 I 在 A 和 D 之前被访问。

练习:广度优先遍历

列出相同树的另外两个可能的广度优先遍历。

答案

由于 F、B 和 D 每个都有两个子节点,因此总共有 2*2*2=8 种可能的广度优先遍历

  1. F B G A D I C E H
  2. F B G A D I E C H
  3. F B G D A I C E H
  4. F B G D A I E C H
  5. F G B I A D H C E
  6. F G B I A D H E C
  7. F G B I D A H C E
  8. F G B I D A H E C

第一个和最后一个遍历已经在上面给出,因此您可以列出任何其他两个。

深度优先

[编辑 | 编辑源代码]

顾名思义,深度优先遍历将尽可能地沿着树的一条分支向下遍历,即直到它在叶子上停止,然后再尝试其他分支。从同一个父节点开始的各种分支可以以任何顺序探索。对于示例树,两种可能的深度优先遍历是 F B A D C E G I H 和 F G I H B D E C A。

练习:广度优先遍历

列出相同树的另外两个可能的深度优先遍历。

答案

由于 F、B 和 D 每个都有两个子节点,因此总共有 2*2*2=8 种可能的深度优先遍历

  1. F B A D C E G I H
  2. F B A D E C G I H
  3. F B D C E A G I H
  4. F B D E C A G I H
  5. F G I H B A D C E
  6. F G I H B A D E C
  7. F G I H B D C E A
  8. F G I H B D E C A

第一个和最后一个遍历已经在上面给出,因此您可以列出任何其他两个。

练习:深度和广度优先遍历

列出此树的 一个 广度优先遍历和 一个 深度优先遍历:

答案

可能的深度优先遍历是

  • G E B D F K M R
  • G E F B D K M R
  • G K M R E B D F
  • G K M R E F B D

可能的广度优先遍历是

  • G E K B F M D R
  • G E K F B M D R
  • G K E M B F R D
  • G K E M F B R D


  • 深度优先遍历通常使用堆栈
  • 广度优先遍历通常使用队列

先序遍历

[编辑 | 编辑源代码]

二叉树通常从左到右遍历,但从右到左的遍历也是可能的,并且可能会出现在考试问题中。因此,在本节的其余部分,我们将始终明确说明方向。

如果每个节点都在访问其两个子树 之前 被访问,则称为 先序 遍历。从左到右的先序遍历的算法是

  1. 访问根节点(通常输出它)
  2. 对左子树进行先序遍历
  3. 对右子树进行先序遍历

可以使用以下方法实现,使用前一个单元中定义的 树数据结构

sub PreOrder(TreeNode)
   Output(TreeNode.value)
   If LeftPointer(TreeNode) != NULL Then
      PreOrder(TreeNode.LeftNode)
   If RightPointer(TreeNode) != NULL Then
      PreOrder(TreeNode.RightNode)
end sub

由于算法完全确定了访问节点的顺序,因此对于每个二叉树,只有一个可能的从左到右的先序遍历。对于我们的示例树,它是一个二叉树,它是 F B A D C E G I H。

由于先序遍历总是沿着一个分支(左或右)向下遍历,然后再移动到另一个分支,因此先序遍历始终是可能的深度优先遍历之一。

后序遍历

[编辑 | 编辑源代码]

如果每个节点都在访问其子树 之后 被访问,则称为 后序 遍历。从左到右的后序遍历的算法是

  1. 对左子树进行后序遍历
  2. 对右子树进行后序遍历
  3. 访问根节点(通常输出它)

可以使用以下方法实现

sub PostOrder(TreeNode)
   If LeftPointer(TreeNode) != NULL Then
      PostOrder(TreeNode.LeftNode)
   If RightPointer(TreeNode) != NULL Then
      PostOrder(TreeNode.RightNode)
   Output(TreeNode.value)
end sub

对于每个二叉树,只有一个从左到右的后序遍历。对于我们的示例树,它是 A C E D B H I G F。

中序遍历

[编辑 | 编辑源代码]

如果每个节点都在访问其左子树和右子树 之间 被访问,则称为 中序 遍历。从左到右的中序遍历的算法是

  1. 对左子树进行中序遍历
  2. 访问根节点(通常输出它)
  3. 对右子树进行中序遍历

可以使用以下方法实现

sub InOrder(TreeNode)
   If LeftPointer(TreeNode) != NULL Then
      InOrder(TreeNode.LeftNode)
   Output(TreeNode.value)
   If RightPointer(TreeNode) != NULL Then
      InOrder(TreeNode.RightNode)
end sub

对于每个二叉树,只有一个从左到右的中序遍历。对于我们的示例树,它是 A B C D E F G H I。请注意,节点是以升序访问的。这不是巧合。

在像我们的示例树这样的二叉搜索树中,左子树中的值小于根,右子树中的值大于根,因此从左到右的中序遍历将以升序访问节点。

练习:中序遍历

语句“中序遍历总是以升序访问节点”是真还是假?

答案

它是错误的。中序遍历只有在它是从左到右的遍历 并且 树是二叉搜索树时,才会以升序访问节点。

如何更改上面的算法以按降序访问二叉搜索树的节点?

答案

从右到左的中序遍历将产生所需的顺序

  1. 对右子树进行中序遍历
  2. 访问根节点(通常输出它)
  3. 对左子树进行中序遍历

遍历技巧

[编辑 | 编辑源代码]

在考试中,你可能会被给出一些遍历伪代码和一棵二叉树,并被要求按照代码访问节点的顺序列出节点。

首先要仔细查看代码并检查

  • 节点是在访问子树之前(先序)、之间(中序)还是之后(后序)访问的?
  • 左子树是在右子树之前访问还是反过来?

例如,以下代码执行从左到右的遍历,注释显示了根节点的访问位置可能在哪里。

sub Traversal(TreeNode)
   'Output(TreeNode.value) REM Pre-Order
   If LeftPointer(TreeNode) != NULL Then
      Traversal(TreeNode.LeftNode)
   'Output(TreeNode.value) REM In-Order
   If RightPointer(TreeNode) != NULL Then
      Traversal(TreeNode.RightNode)
   'Output(TreeNode.value)  REM Post-Order
end sub

假设它是从左到右的遍历。一旦你知道从节点访问的位置,是先序、中序还是后序遍历,你就可以按照如下方式标注二叉树的每个节点:

遍历类型 输出代码位置 放置标记的位置
先序 顶部 左侧
中序 中间 底部
后序 底部 右侧

最后,围绕树逆时针画一条线,连接这些标记。沿着这条线,在遇到标记时写下每个节点:这将是代码访问节点的顺序。以下是三种可能的从左到右遍历。

如你所见,遵循这个技巧,你将得到与上面部分相同的答案。

如果遍历是从右到左,你必须画一条顺时针线,并交换先序或后序标记的位置。

遍历类型 输出代码位置 从左到右遍历的标记 从右到左遍历的标记
先序 顶部 左侧 右侧
中序 中间 底部 底部
后序 底部 右侧 左侧

如果树是二叉搜索树,并且要求你进行中序遍历,你应该按照升序(对于从左到右的遍历)或降序(对于从右到左的遍历)访问节点。如果它不是二叉搜索树,中序遍历不会按照升序或降序访问节点,但先序或后序遍历可能会,这将取决于节点在树中的位置。

虽然在本节中,为了清晰起见,我们始终明确地说明是左到右还是右到左遍历,但先序、中序和后序术语的典型用法隐含着左到右遍历,除非有相反的说明。

练习:二叉树遍历

对于以下二叉树,执行先序、中序和后序遍历。

答案

如果没有其他说明,则假定从左到右遍历。

  • 先序遍历:GEBDFKMR
  • 中序遍历:BDEFGKMR
  • 后序遍历:DBFERMKG

以下代码描述了哪种遍历

sub Traverse(TreeNode)
   If LeftPointer(TreeNode) != NULL Then
      Traverse(TreeNode.LeftNode)
   Output(TreeNode.value)
   If RightPointer(TreeNode) != NULL Then
      Traverse(TreeNode.RightNode)
end sub

答案

中序遍历,因为节点访问位于代码的中心,并且左子树在右子树之前遍历。

以下代码的作用是什么?

sub P(TreeNode)
   If RightPointer(TreeNode) != NULL Then
      P(TreeNode.RightNode)
   If LeftPointer(TreeNode) != NULL Then
      P(TreeNode.LeftNode)
   Output(TreeNode.value)
end sub

答案

它执行从右到左的后序遍历,因为它首先访问右子树,然后访问左子树,最后访问节点。

使用以下二叉树

从右到左的输出结果将是什么

  • 先序遍历
  • 中序遍历
  • 后序遍历

答案

  • 从右到左的先序遍历:7 8 1 9 5 3 4 2
  • 从右到左的中序遍历:1 8 9 7 3 5 2 4
  • 从右到左的后序遍历:1 9 8 3 2 4 5 7
华夏公益教科书