mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-14 08:06:06 +00:00
build
This commit is contained in:
@@ -10,10 +10,14 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. AVL 树在删除结点后发生退化 </p>
|
||||
|
||||
再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. AVL 树在插入结点后发生退化 </p>
|
||||
|
||||
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除结点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。
|
||||
|
||||
换言之,在频繁增删查改的使用场景中,AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。
|
||||
@@ -470,6 +474,8 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 有 grandChild 的右旋操作 </p>
|
||||
|
||||
“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。
|
||||
|
||||
=== "Java"
|
||||
@@ -646,10 +652,14 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 左旋操作 </p>
|
||||
|
||||
同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 有 grandChild 的左旋操作 </p>
|
||||
|
||||
观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
|
||||
|
||||
=== "Java"
|
||||
@@ -826,18 +836,24 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 先左旋后右旋 </p>
|
||||
|
||||
### Case 4 - 先右后左
|
||||
|
||||
同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 先右旋后左旋 </p>
|
||||
|
||||
### 旋转的选择
|
||||
|
||||
下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. AVL 树的四种旋转情况 </p>
|
||||
|
||||
具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -11,6 +11,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树 </p>
|
||||
|
||||
## 7.3.1. 二叉搜索树的操作
|
||||
|
||||
### 查找结点
|
||||
@@ -249,6 +251,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉搜索树中插入结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_search_tree.java"
|
||||
@@ -551,10 +555,14 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 0) </p>
|
||||
|
||||
**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉搜索树中删除结点(度为 1) </p>
|
||||
|
||||
**当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步:
|
||||
|
||||
1. 找到待删除结点在 **中序遍历序列** 中的下一个结点,记为 `nex` ;
|
||||
@@ -1137,6 +1145,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树的中序遍历序列 </p>
|
||||
|
||||
## 7.3.2. 二叉搜索树的效率
|
||||
|
||||
假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:
|
||||
@@ -1178,6 +1188,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树的平衡与退化 </p>
|
||||
|
||||
## 7.3.4. 二叉搜索树常见应用
|
||||
|
||||
- 系统中的多级索引,高效查找、插入、删除操作。
|
||||
|
||||
@@ -133,6 +133,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 父结点、子结点、子树 </p>
|
||||
|
||||
## 7.1.1. 二叉树常见术语
|
||||
|
||||
二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。
|
||||
@@ -148,6 +150,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的常用术语 </p>
|
||||
|
||||
!!! tip "高度与深度的定义"
|
||||
|
||||
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。
|
||||
@@ -306,6 +310,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉树中插入与删除结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_tree.java"
|
||||
@@ -428,6 +434,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完美二叉树 </p>
|
||||
|
||||
### 完全二叉树
|
||||
|
||||
「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点尽量靠左填充。
|
||||
@@ -436,18 +444,24 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完全二叉树 </p>
|
||||
|
||||
### 完满二叉树
|
||||
|
||||
「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完满二叉树 </p>
|
||||
|
||||
### 平衡二叉树
|
||||
|
||||
「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 平衡二叉树 </p>
|
||||
|
||||
## 7.1.4. 二叉树的退化
|
||||
|
||||
当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。
|
||||
@@ -457,6 +471,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的最佳与最二叉树的最佳和最差结构差情况 </p>
|
||||
|
||||
如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
@@ -480,10 +496,14 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完美二叉树的数组表示 </p>
|
||||
|
||||
然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 给定数组对应多种二叉树可能性 </p>
|
||||
|
||||
为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。
|
||||
|
||||
=== "Java"
|
||||
@@ -563,8 +583,12 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
|
||||
|
||||
回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 完全二叉树的数组表示 </p>
|
||||
|
||||
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。
|
||||
|
||||
@@ -16,6 +16,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的层序遍历 </p>
|
||||
|
||||
### 算法实现
|
||||
|
||||
广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。
|
||||
@@ -256,6 +258,8 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉搜索树的前、中、后序遍历 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 位置 | 含义 | 此处访问结点时对应 |
|
||||
|
||||
Reference in New Issue
Block a user