---
marp: true
theme: kurzus
paginate: true
footer: 数据结构 · 第 14 周 · 树、二叉树、遍历
---

<!-- _class: cim -->
<!-- _paginate: false -->
<!-- _footer: "" -->

# 数据结构

## 第 14 周 – 树：基本概念、二叉树、存储表示、遍历

Koczka Ferenc 博士 · 埃斯特哈齐·卡罗伊大学 · 2026

---

## 复习

- 到目前为止都是<strong>线性</strong>数据结构：数组、链表、栈、队列 – 每个元素至多有<strong>一个</strong>后继
- <strong>递归</strong>（第 12 周）：把问题归结为较小的实例
- <strong>栈</strong>和<strong>队列</strong>：遍历的工具

### 今天的问题

- 怎样存储<strong>层次</strong>数据？
- 什么是二叉树？它在内存中怎样表示？
- 怎样访问树的每一个结点？

<!--
时间安排：基本概念 20' · 二叉树、类型 15' · 存储表示 20' · 遍历 25' · 递归操作 10'
旧课程第 8 周和第 11 周的内容；旧试卷中用数组表示树的题目属于这一部分。
-->

---

## 层次数据

很多数据天然是<strong>树形</strong>的：

- <strong>文件系统：</strong>文件夹、子文件夹、文件
- <strong>组织结构图：</strong>大学 → 学院 → 研究所 → 系
- <strong>家谱</strong>
- <strong>网页</strong>（HTML DOM）：`<html>` → `<body>` → `<div>` → …
- <strong>表达式：</strong>`(3 + 4) * (5 − 2)`
- <strong>决策：</strong>象棋程序的走法树、机器学习中的决策树

<div class="doboz">

树是一种<strong>层次</strong>数据结构：每个元素<strong>至多有一个前驱</strong>（父结点），但可以有<strong>多个后继</strong>（孩子）（第 1 周：分类）。

</div>

---

## 基本概念

![w:1000](kepek/h14_fogalmak.svg)

---

## 基本概念 – 汇总

| 概念 | 含义 |
|---|---|
| <strong>结点</strong>、<strong>边</strong> | 元素，以及父子之间的连接 |
| <strong>根</strong> | 唯一没有父结点的结点 |
| <strong>叶子</strong> | 没有孩子的结点 |
| <strong>父结点、孩子、兄弟</strong> | 直接的连接 · 同一个父结点的孩子 |
| <strong>祖先、子孙</strong> | 通向根的路径上的结点，或者向下的结点 |
| <strong>子树</strong> | 一个结点及其所有子孙 |
| <strong>深度 / 层</strong> | 从根出发的路径长度（根为 0） |
| <strong>高度</strong> | 最长的根–叶子路径的长度 |
| <strong>度</strong> | 孩子的个数 · <strong>森林：</strong>树的集合 |

<strong>递归定义：</strong>树或者是<strong>空</strong>的，或者由一个<strong>根</strong>及其<strong>子树</strong>组成。

---

<!-- _class: fejezet -->

# 二叉树

---

## 二叉树

<strong>二叉树：</strong>每个结点<strong>至多有两个</strong>孩子：<strong>左</strong>孩子和<strong>右</strong>孩子。

![w:1000](kepek/h14_tipusok.svg)

---

## 二叉树的性质

- 第 <em>i</em> 层至多有 <strong>2ⁱ</strong> 个结点
- 高度为 <em>h</em> 的二叉树至多有 <strong>2ʰ⁺¹ − 1</strong> 个结点（完美二叉树）
- 有 <em>n</em> 个结点的二叉树，高度至少为 <strong>⌊log₂ n⌋</strong>，至多为 <strong>n − 1</strong>（退化）

| 性质 | 二叉树 |
|---|---|
| 元素 | 同构 |
| 关系 | <strong>层次</strong> |
| 大小 | 动态 |
| 存储 | 链式或顺序（数组） |
| 访问 | <strong>顺序</strong>（从根开始，沿指针） |

<div class="doboz tipp">

高度决定了树的操作的<strong>步数</strong> – 所以树是否<strong>平衡</strong>很重要（第 15 周）。

</div>

---

<!-- _class: fejezet -->

# 二叉树的存储表示

---

<!-- _class: kicsi -->

## 链式表示

![w:760](kepek/h14_lancolt.svg)

<div class="ket-hasab">
<div>

<span class="cimke java">Java</span>

```java
class Node {
    char value;
    Node left, right;
}
Node root;   // null:空树
```

</div>
<div>

<span class="cimke go">Go</span>

```go
type Node struct {
    Value       rune
    Left, Right *Node
}
```

</div>
</div>

伪代码：`Elem[P].Value`、`Elem[P].Left`、`Elem[P].Right`,<strong>Root</strong> 指针指向根。

---

## 数组（顺序）表示

![w:1000](kepek/h14_tombos.svg)

- 不需要指针：孩子和父结点的下标<strong>可以计算</strong>
- 对<strong>完全</strong>二叉树非常合适（没有空位） – <strong>堆</strong>就使用这种表示（第 15 周）
- 对退化的树会有大量空位：高度为 <em>h</em> 的链需要 2ʰ⁺¹ − 1 个元素的数组！

---

## 用二叉树表示一般的树

如果一个结点可以有<strong>任意</strong>个孩子：<strong>左孩子 – 右兄弟</strong>表示法

- <strong>左</strong>指针 → <strong>第一个孩子</strong>
- <strong>右</strong>指针 → <strong>下一个兄弟</strong>

<div class="ket-hasab">
<div>

```text
        A
      / | \
     B  C  D
    / \     \
   E   F     G
```

</div>
<div>

```text
   A
  /
 B ———— C ———— D
 |             |
 E ——— F       G
```

</div>
</div>

这样每一棵一般的树（以及森林）都可以用<strong>二叉树</strong>存储 – 例如文件系统的文件夹结构。

---

<!-- _class: fejezet -->

# 遍历

---

## 树的遍历

<strong>遍历：</strong>按照一定的顺序，每个结点<strong>恰好处理一次</strong>。

![w:1000](kepek/h14_bejaras.svg)

---

<!-- _class: kicsi tomor -->

## 深度优先遍历 – 递归实现

<div class="ket-hasab">
<div>

<span class="cimke pszeudo">伪代码</span>

```text
Procedure Preorder(P)
  If P ≠ Nil then
    Process(Elem[P].Value)
    Preorder(Elem[P].Left)
    Preorder(Elem[P].Right)
  End if
End procedure

Procedure Inorder(P)
  If P ≠ Nil then
    Inorder(Elem[P].Left)
    Process(Elem[P].Value)
    Inorder(Elem[P].Right)
  End if
End procedure

Procedure Postorder(P)
  If P ≠ Nil then
    Postorder(Elem[P].Left)
    Postorder(Elem[P].Right)
    Process(Elem[P].Value)
  End if
End procedure
```

</div>
<div>

<span class="cimke java">Java</span>

```java
static void inorder(Node p) {
    if (p == null) return;
    inorder(p.left);
    System.out.print(p.value + " ");
    inorder(p.right);
}
```

<span class="cimke go">Go</span>

```go
func inorder(p *Node) {
    if p == nil {
        return
    }
    inorder(p.Left)
    fmt.Printf("%c ", p.Value)
    inorder(p.Right)
}
```

三种遍历的<strong>唯一</strong>区别是<strong>何时</strong>处理根。步数：<strong>O(n)</strong>，空间：O(h)。

</div>
</div>

---

## 层序（广度优先）遍历

从根开始，<strong>逐层</strong>从左到右 – 用<strong>队列</strong>（第 8 周）：

```text
Procedure LevelOrder(Root)
  If Root = Nil then exit
  Enqueue(Root)
  While not IsEmpty
    P ← Dequeue
    Process(Elem[P].Value)
    If Elem[P].Left ≠ Nil then Enqueue(Elem[P].Left)
    If Elem[P].Right ≠ Nil then Enqueue(Elem[P].Right)
  End while
End procedure
```

例：<strong>F B G A D I C E H</strong> · 如果把队列换成<strong>栈</strong>（并且先压入右孩子），得到的就是<strong>先序</strong>遍历！

---

## 应用：表达式树

![w:1000](kepek/h14_kifejezesfa.svg)

- 内部结点是<strong>运算符</strong>，叶子是<strong>操作数</strong>
- <strong>后序</strong>遍历正好给出<strong>后缀</strong>形式 – 可以用栈由后缀形式构造出这棵树
- <strong>求值：</strong>`Value(P) = Value(left) op Value(right)` – 递归地计算

---

<!-- _class: kicsi -->

## 树上的递归操作

模式：<strong>“处理根，再对两棵子树调用自身”</strong>

<div class="ket-hasab">
<div>

```text
Function Count(P)
  If P = Nil then Return 0
  Return 1 + Count(Elem[P].Left)
           + Count(Elem[P].Right)
End function

Function Height(P)
  If P = Nil then Return −1
  Return 1 + max(Height(Elem[P].Left),
                 Height(Elem[P].Right))
End function
```

</div>
<div>

```text
Function Leaves(P)
  If P = Nil then Return 0
  If Elem[P].Left = Nil and Elem[P].Right = Nil
    then Return 1
  Return Leaves(Elem[P].Left)
       + Leaves(Elem[P].Right)
End function
```

其他例子：求和、最大值、查找（O(n)）、镜像、复制、比较两棵树

</div>
</div>

<span class="halvany">对于示例树：9 个结点，4 片叶子（A、C、E、H），高度为 3。</span>

---

## 小结

- <strong>树：</strong>层次数据结构；根、叶子、父结点、孩子、子树、深度、高度
- <strong>二叉树：</strong>至多两个孩子；满、完全、完美、退化
- <strong>链式表示：</strong>Left 和 Right 指针 · <strong>数组表示：</strong>孩子 2i 和 2i + 1，父结点 i div 2
- <strong>遍历：</strong>先序（根-左-右）、中序（左-根-右）、后序（左-右-根） – 递归，O(n)；层序 – 用队列
- 树上的操作<strong>天然是递归的</strong>

<strong>下周：</strong>二叉搜索树 – 查找、插入、删除；平衡；堆。

---

## 复习题

1. 判断对错：二叉树是层次数据结构。
2. 写出遍历序列：根为 1;1 的左孩子是 2，右孩子是 3;2 的孩子是 4 和 5;3 的右孩子是 6。
3. 二叉树的元素存放在数组 A 中（A[1] 是根，第 <em>i</em> 个元素的孩子是 A[2i] 和 A[2i + 1]）。A = [K, F, R, B, H, –, T]。画出这棵树，并写出中序遍历！
4. 高度为 4 的二叉树至多有多少个结点？
5. 哪种遍历能从表达式树得到后缀形式？
6. 只有一个结点的树，`Height(P)` 的值是多少？

<!--
答案：1. 对
2. 先序：1 2 4 5 3 6 · 中序：4 2 5 1 3 6 · 后序：4 5 2 6 3 1 · 层序：1 2 3 4 5 6
3. K 是根；F（左）、R（右）；F 的孩子是 B、H;R 的右孩子是 T。中序：B F H K R T
4. 2^5 − 1 = 31   5. 后序   6. 0
-->

---

<!-- _class: fejezet -->
<!-- _footer: "" -->

# 实践课

---

<!-- _class: gyak -->

## 示例程序

| 程序 | Java | Go | 演示内容 |
|---|---|---|---|
| 二叉树 | `BinaryTree.java` | `w14/binarytree` | 四种遍历、结点数、叶子数、高度、横向打印 |
| 数组表示的树 | `ArrayTree.java` | `w14/arraytree` | 下标公式、用下标遍历 |
| 表达式树 | `ExpressionTree.java` | `w14/expressiontree` | 由后缀形式构造、求值、中缀/前缀 |
| 用栈遍历 | `IterativeTraversal.java` | `w14/iterative` | 不用递归的先序和中序遍历 |

---

<!-- _class: gyak -->

## 练习

1. <strong>和与最大值：</strong>计算存放整数的二叉树中元素的和与最大值！
2. <strong>只有一个孩子的结点：</strong>有多少个结点恰好有一个孩子？
3. <strong>满二叉树：</strong>判断是否每个结点都有 0 个或 2 个孩子！
4. <strong>第 k 层：</strong>输出第 <em>k</em> 层的元素！
5. <strong>路径：</strong>输出所有根–叶子路径！
6. <strong>相同的树：</strong>判断两棵二叉树的结构和值是否都相同！

参考答案：`Exercises14.java`、`w14/exercises`

---

<!-- _class: gyak -->

## 课后作业

1. 编写函数，把链式表示的二叉树转换成<strong>数组</strong>表示（以及反过来）！需要多大的数组？
2. 由<strong>先序</strong>和<strong>中序</strong>遍历序列构造二叉树！（例如先序：F B A D C E G I H，中序：A B C D E F G H I）
3. <strong>逐层、每层一行</strong>地输出这棵树（每一层另起一行）！
4. 编写程序，以<strong>树</strong>的形式输出一个文件夹的内容（就像 `tree` 命令）！（Java:`File.listFiles()`,Go:`os.ReadDir`）

<!--
解题思路：
1. 数组大小：2^(h+1) − 1（h 为高度）；根放在下标 1，左孩子 2i，右孩子 2i+1 – 用递归遍历。
2. 先序序列的第一个元素是根；在中序序列中，它左边是左子树的元素，右边是右子树的元素；递归处理。
3. 广度优先遍历；每一层开始时记下队列的大小，取出那么多个元素。
4. 递归遍历，按深度缩进（先序）。
-->
