hello-algo/docs/chapter_tree/binary_tree.md
2023-04-13 22:44:59 +08:00

566 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 二叉树
「二叉树 Binary Tree」是一种非线性数据结构代表着祖先与后代之间的派生关系体现着“一分为二”的分治逻辑。与链表类似二叉树的基本单元是节点每个节点包含一个「值」和两个「指针」。
=== "Java"
```java title=""
/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode left; // 左子节点指针
TreeNode right; // 右子节点指针
TreeNode(int x) { val = x; }
}
```
=== "C++"
```cpp title=""
/* 二叉树节点结构体 */
struct TreeNode {
int val; // 节点值
TreeNode *left; // 左子节点指针
TreeNode *right; // 右子节点指针
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
```
=== "Python"
```python title=""
class TreeNode:
"""二叉树节点类"""
def __init__(self, val: int):
self.val: int = val # 节点值
self.left: Optional[TreeNode] = None # 左子节点指针
self.right: Optional[TreeNode] = None # 右子节点指针
```
=== "Go"
```go title=""
/* 二叉树节点结构体 */
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
/* 节点初始化方法 */
func NewTreeNode(v int) *TreeNode {
return &TreeNode{
Left: nil,
Right: nil,
Val: v,
}
}
```
=== "JavaScript"
```javascript title=""
/* 二叉树节点类 */
function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val); // 节点值
this.left = (left === undefined ? null : left); // 左子节点指针
this.right = (right === undefined ? null : right); // 右子节点指针
}
```
=== "TypeScript"
```typescript title=""
/* 二叉树节点类 */
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val; // 节点值
this.left = left === undefined ? null : left; // 左子节点指针
this.right = right === undefined ? null : right; // 右子节点指针
}
}
```
=== "C"
```c title=""
```
=== "C#"
```csharp title=""
/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode? left; // 左子节点指针
TreeNode? right; // 右子节点指针
TreeNode(int x) { val = x; }
}
```
=== "Swift"
```swift title=""
/* 二叉树节点类 */
class TreeNode {
var val: Int // 节点值
var left: TreeNode? // 左子节点指针
var right: TreeNode? // 右子节点指针
init(x: Int) {
val = x
}
}
```
=== "Zig"
```zig title=""
```
节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。
**在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点则其左子节点和右子节点分别是“节点 4”和“节点 5”左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
![父节点、子节点、子树](binary_tree.assets/binary_tree_definition.png)
## 二叉树常见术语
二叉树涉及的术语较多,建议尽量理解并记住。
- 「根节点 Root Node」位于二叉树顶层的节点没有父节点
- 「叶节点 Leaf Node」没有子节点的节点其两个指针均指向 $\text{null}$
- 节点的「层 Level」从顶至底递增根节点所在层为 1
- 节点的「度 Degree」节点的子节点的数量。在二叉树中度的范围是 0, 1, 2
- 「边 Edge」连接两个节点的线段即节点指针
- 二叉树的「高度」:从根节点到最远叶节点所经过的边的数量;
- 节点的「深度 Depth」 :从根节点到该节点所经过的边的数量;
- 节点的「高度 Height」从最远叶节点到该节点所经过的边的数量
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
!!! tip "高度与深度的定义"
请注意,我们通常将「高度」和「深度」定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。
## 二叉树基本操作
**初始化二叉树**。与链表类似,首先初始化节点,然后构建引用指向(即指针)。
=== "Java"
```java title="binary_tree.java"
// 初始化节点
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "C++"
```cpp title="binary_tree.cpp"
/* 初始化二叉树 */
// 初始化节点
TreeNode* n1 = new TreeNode(1);
TreeNode* n2 = new TreeNode(2);
TreeNode* n3 = new TreeNode(3);
TreeNode* n4 = new TreeNode(4);
TreeNode* n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5;
```
=== "Python"
```python title="binary_tree.py"
# 初始化二叉树
# 初始化节点
n1 = TreeNode(val=1)
n2 = TreeNode(val=2)
n3 = TreeNode(val=3)
n4 = TreeNode(val=4)
n5 = TreeNode(val=5)
# 构建引用指向(即指针)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "Go"
```go title="binary_tree.go"
/* 初始化二叉树 */
// 初始化节点
n1 := NewTreeNode(1)
n2 := NewTreeNode(2)
n3 := NewTreeNode(3)
n4 := NewTreeNode(4)
n5 := NewTreeNode(5)
// 构建引用指向(即指针)
n1.Left = n2
n1.Right = n3
n2.Left = n4
n2.Right = n5
```
=== "JavaScript"
```javascript title="binary_tree.js"
/* 初始化二叉树 */
// 初始化节点
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "TypeScript"
```typescript title="binary_tree.ts"
/* 初始化二叉树 */
// 初始化节点
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
n4 = new TreeNode(4),
n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "C"
```c title="binary_tree.c"
```
=== "C#"
```csharp title="binary_tree.cs"
/* 初始化二叉树 */
// 初始化节点
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
```
=== "Swift"
```swift title="binary_tree.swift"
// 初始化节点
let n1 = TreeNode(x: 1)
let n2 = TreeNode(x: 2)
let n3 = TreeNode(x: 3)
let n4 = TreeNode(x: 4)
let n5 = TreeNode(x: 5)
// 构建引用指向(即指针)
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
```
=== "Zig"
```zig title="binary_tree.zig"
```
**插入与删除节点**。与链表类似,通过修改指针来实现插入与删除节点。
![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png)
=== "Java"
```java title="binary_tree.java"
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1.left = P;
P.left = n2;
// 删除节点 P
n1.left = n2;
```
=== "C++"
```cpp title="binary_tree.cpp"
/* 插入与删除节点 */
TreeNode* P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1->left = P;
P->left = n2;
// 删除节点 P
n1->left = n2;
```
=== "Python"
```python title="binary_tree.py"
# 插入与删除节点
p = TreeNode(0)
# 在 n1 -> n2 中间插入节点 P
n1.left = p
p.left = n2
# 删除节点 P
n1.left = n2
```
=== "Go"
```go title="binary_tree.go"
/* 插入与删除节点 */
// 在 n1 -> n2 中间插入节点 P
p := NewTreeNode(0)
n1.Left = p
p.Left = n2
// 删除节点 P
n1.Left = n2
```
=== "JavaScript"
```javascript title="binary_tree.js"
/* 插入与删除节点 */
let P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1.left = P;
P.left = n2;
// 删除节点 P
n1.left = n2;
```
=== "TypeScript"
```typescript title="binary_tree.ts"
/* 插入与删除节点 */
const P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1.left = P;
P.left = n2;
// 删除节点 P
n1.left = n2;
```
=== "C"
```c title="binary_tree.c"
```
=== "C#"
```csharp title="binary_tree.cs"
/* 插入与删除节点 */
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中间插入节点 P
n1.left = P;
P.left = n2;
// 删除节点 P
n1.left = n2;
```
=== "Swift"
```swift title="binary_tree.swift"
let P = TreeNode(x: 0)
// 在 n1 -> n2 中间插入节点 P
n1.left = P
P.left = n2
// 删除节点 P
n1.left = n2
```
=== "Zig"
```zig title="binary_tree.zig"
```
!!! note
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。
## 常见二叉树类型
### 完美二叉树
「完美二叉树 Perfect Binary Tree」除了最底层外其余所有层的节点都被完全填满。在完美二叉树中叶节点的度为 $0$ ,其余所有节点的度都为 $2$ ;若树高度为 $h$ ,则节点总数为 $2^{h+1} - 1$ ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。
!!! tip
在中文社区中,完美二叉树常被称为「满二叉树」,请注意区分。
![完美二叉树](binary_tree.assets/perfect_binary_tree.png)
### 完全二叉树
「完全二叉树 Complete Binary Tree」只有最底层的节点未被填满且最底层节点尽量靠左填充。
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
### 完满二叉树
「完满二叉树 Full Binary Tree」除了叶节点之外其余所有节点都有两个子节点。
![完满二叉树](binary_tree.assets/full_binary_tree.png)
### 平衡二叉树
「平衡二叉树 Balanced Binary Tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
## 二叉树的退化
当二叉树的每层节点都被填满时,达到「完美二叉树」;而当所有节点都偏向一侧时,二叉树退化为「链表」。
- 完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势;
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$
![二叉树的最佳与最差结构](binary_tree.assets/binary_tree_corner_cases.png)
如下表所示,在最佳和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大或极小值。
<div class="center-table" markdown>
| | 完美二叉树 | 链表 |
| ----------------------------- | ---------- | ---------- |
| 第 $i$ 层的节点数量 | $2^{i-1}$ | $1$ |
| 树的高度为 $h$ 时的叶节点数量 | $2^h$ | $1$ |
| 树的高度为 $h$ 时的节点总数 | $2^{h+1} - 1$ | $h + 1$ |
| 树的节点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
</div>
## 二叉树表示方式 *
我们通常使用二叉树的「链表表示」,即存储单位为节点 `TreeNode` ,节点之间通过指针相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。
那么,能否用「数组」来表示二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将节点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父节点索引与子节点索引之间的“映射公式”:**若节点的索引为 $i$ ,则该节点的左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$** 。
**本质上,映射公式的作用相当于链表中的指针**。对于层序遍历序列中的任意节点,我们都可以使用映射公式来访问其子节点。因此,我们可以将二叉树的层序遍历序列存储到数组中,利用以上映射公式来表示二叉树。
![完美二叉树的数组表示](binary_tree.assets/array_representation_mapping.png)
然而,完美二叉树只是一个特例。在二叉树的中间层,通常存在许多 $\text{null}$ ,而层序遍历序列并不包含这些 $\text{null}$ 。我们无法仅凭序列来推测空节点的数量和分布位置,**这意味着理论上存在许多种二叉树都符合该层序遍历序列**。显然,在这种情况下,我们无法使用数组来存储二叉树。
![给定数组对应多种二叉树可能性](binary_tree.assets/array_representation_without_empty.png)
为了解决这个问题,我们可以考虑按照完美二叉树的形式来表示所有二叉树,**并在序列中使用特殊符号来显式地表示 $\text{null}$**。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。
=== "Java"
```java title=""
/* 二叉树的数组表示 */
// 使用 int 的包装类 Integer ,就可以使用 null 来标记空位
Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
```
=== "C++"
```cpp title=""
/* 二叉树的数组表示 */
// 为了符合数据类型为 int ,使用 int 最大值标记空位
// 该方法的使用前提是没有节点的值 = INT_MAX
vector<int> tree = { 1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15 };
```
=== "Python"
```python title=""
# 二叉树的数组表示
# 直接使用 None 来表示空位
tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]
```
=== "Go"
```go title=""
/* 二叉树的数组表示 */
// 使用 any 类型的切片, 就可以使用 nil 来标记空位
tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}
```
=== "JavaScript"
```javascript title=""
/* 二叉树的数组表示 */
// 直接使用 null 来表示空位
let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "TypeScript"
```typescript title=""
/* 二叉树的数组表示 */
// 直接使用 null 来表示空位
let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];
```
=== "C"
```c title=""
```
=== "C#"
```csharp title=""
/* 二叉树的数组表示 */
// 使用 int? 可空类型 ,就可以使用 null 来标记空位
int?[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };
```
=== "Swift"
```swift title=""
/* 二叉树的数组表示 */
// 使用 Int? 可空类型 ,就可以使用 nil 来标记空位
let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]
```
=== "Zig"
```zig title=""
```
![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png)
**完全二叉树非常适合使用数组来表示**。回顾「完全二叉树」的定义,$\text{null}$ 只出现在最底层,并且最底层的节点尽量靠左。这意味着,**所有空节点一定出现在层序遍历序列的末尾**。由于我们事先知道了所有 $\text{null}$ 的位置,因此在使用数组表示完全二叉树时,可以省略存储它们。
![完全二叉树的数组表示](binary_tree.assets/array_representation_complete_binary_tree.png)
数组表示具有两个显著优点:首先,它不需要存储指针,从而节省了空间;其次,它允许随机访问节点。然而,当二叉树中存在大量 $\text{null}$ 时,数组中包含的节点数据比重较低,导致有效空间利用率降低。