refactor: Replace 结点 with 节点 (#452)
* Replace 结点 with 节点 Update the footnotes in the figures * Update mindmap * Reduce the size of the mindmap.png
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 57 KiB |
@@ -1,18 +1,18 @@
|
||||
# 建堆操作 *
|
||||
|
||||
如果我们想要根据输入列表来生成一个堆,这样的操作被称为「建堆」。
|
||||
如果我们想要根据输入列表生成一个堆,这个过程被称为「建堆」。
|
||||
|
||||
## 两种建堆方法
|
||||
|
||||
### 借助入堆方法实现
|
||||
|
||||
最直接地,考虑借助「元素入堆」方法,先建立一个空堆,**再将列表元素依次入堆即可**。
|
||||
最直接的方法是借助“元素入堆操作”实现,首先创建一个空堆,然后将列表元素依次添加到堆中。
|
||||
|
||||
设元素数量为 $n$ ,则最后一个元素入堆的时间复杂度为 $O(\log n)$ ,在依次入堆时,堆的平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。
|
||||
设元素数量为 $n$ ,则最后一个元素入堆的时间复杂度为 $O(\log n)$ 。在依次添加元素时,堆的平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。
|
||||
|
||||
### 基于堆化操作实现
|
||||
|
||||
有趣的是,存在一种更加高效的建堆方法,时间复杂度可以达到 $O(n)$ 。我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。
|
||||
有趣的是,存在一种更高效的建堆方法,其时间复杂度仅为 $O(n)$ 。我们先将列表所有元素原封不动添加到堆中,**然后迭代地对各个节点执行“从顶至底堆化”**。当然,**我们不需要对叶节点执行堆化操作**,因为它们没有子节点。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@@ -76,24 +76,24 @@
|
||||
|
||||
## 复杂度分析
|
||||
|
||||
第二种建堆方法的时间复杂度为什么是 $O(n)$ 呢?我们来展开推算一下。
|
||||
为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。
|
||||
|
||||
- 完全二叉树中,设结点总数为 $n$ ,则叶结点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此在排除叶结点后,需要堆化结点数量为 $(n - 1)/2$ ,即为 $O(n)$ ;
|
||||
- 从顶至底堆化中,每个结点最多堆化至叶结点,因此最大迭代次数为二叉树高度 $O(\log n)$ ;
|
||||
- 完全二叉树中,设节点总数为 $n$ ,则叶节点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此,在排除叶节点后,需要堆化的节点数量为 $(n - 1)/2$ ,复杂度为 $O(n)$ ;
|
||||
- 在从顶至底堆化的过程中,每个节点最多堆化到叶节点,因此最大迭代次数为二叉树高度 $O(\log n)$ ;
|
||||
|
||||
将上述两者相乘,可得时间复杂度为 $O(n \log n)$ 。这个估算结果不够准确,因为我们没有考虑到 **二叉树底层结点远多于顶层结点** 的性质。
|
||||
将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$。**然而,这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的特性**。
|
||||
|
||||
下面我们来展开计算。为了减小计算难度,我们假设树是一个「完美二叉树」,该假设不会影响计算结果的正确性。设二叉树(即堆)结点数量为 $n$ ,树高度为 $h$ 。上文提到,**结点堆化最大迭代次数等于该结点到叶结点的距离,而这正是“结点高度”**。
|
||||
接下来我们来进行更为详细的计算。为了减小计算难度,我们假设树是一个“完美二叉树”,该假设不会影响计算结果的正确性。设二叉树(即堆)节点数量为 $n$ ,树高度为 $h$ 。上文提到,**节点堆化最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。
|
||||
|
||||

|
||||

|
||||
|
||||
因此,我们将各层的“结点数量 $\times$ 结点高度”求和,即可得到 **所有结点的堆化的迭代次数总和**。
|
||||
因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。
|
||||
|
||||
$$
|
||||
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1
|
||||
$$
|
||||
|
||||
化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得
|
||||
化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,得到
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -108,7 +108,7 @@ $$
|
||||
2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \cdots + 2^{h-1} + 2^h
|
||||
$$
|
||||
|
||||
观察上式,$T(h)$ 是一个等比数列,可直接使用求和公式,得到时间复杂度为
|
||||
观察上式,发现 $T(h)$ 是一个等比数列,可直接使用求和公式,得到时间复杂度为
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -118,4 +118,4 @@ T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
进一步地,高度为 $h$ 的完美二叉树的结点数量为 $n = 2^{h+1} - 1$ ,易得复杂度为 $O(2^h) = O(n)$ 。以上推算表明,**输入列表并建堆的时间复杂度为 $O(n)$ ,非常高效**。
|
||||
进一步地,高度为 $h$ 的完美二叉树的节点数量为 $n = 2^{h+1} - 1$ ,易得复杂度为 $O(2^h) = O(n)$ 。以上推算表明,**输入列表并建堆的时间复杂度为 $O(n)$ ,非常高效**。
|
||||
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 108 KiB |
@@ -2,16 +2,16 @@
|
||||
|
||||
「堆 Heap」是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型:
|
||||
|
||||
- 「大顶堆 Max Heap」,任意结点的值 $\geq$ 其子结点的值;
|
||||
- 「小顶堆 Min Heap」,任意结点的值 $\leq$ 其子结点的值;
|
||||
- 「大顶堆 Max Heap」,任意节点的值 $\geq$ 其子节点的值;
|
||||
- 「小顶堆 Min Heap」,任意节点的值 $\leq$ 其子节点的值;
|
||||
|
||||

|
||||
|
||||
## 堆术语与性质
|
||||
|
||||
- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。
|
||||
- 二叉树中的根结点对应「堆顶」,底层最靠右结点对应「堆底」。
|
||||
- 对于大顶堆 / 小顶堆,其堆顶元素(即根结点)的值最大 / 最小。
|
||||
- 由于堆是完全二叉树,因此最底层节点靠左填充,其它层节点皆被填满。
|
||||
- 二叉树中的根节点对应「堆顶」,底层最靠右节点对应「堆底」。
|
||||
- 对于大顶堆 / 小顶堆,其堆顶元素(即根节点)的值最大 / 最小。
|
||||
|
||||
## 堆常用操作
|
||||
|
||||
@@ -308,9 +308,9 @@
|
||||
|
||||
在二叉树章节我们学过,「完全二叉树」非常适合使用「数组」来表示,而堆恰好是一棵完全二叉树,**因而我们采用「数组」来存储「堆」**。
|
||||
|
||||
**二叉树指针**。使用数组表示二叉树时,元素代表结点值,索引代表结点在二叉树中的位置,**而结点指针通过索引映射公式来实现**。
|
||||
**二叉树指针**。使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置,**而节点指针通过索引映射公式来实现**。
|
||||
|
||||
具体地,给定索引 $i$ ,那么其左子结点索引为 $2i + 1$ 、右子结点索引为 $2i + 2$ 、父结点索引为 $(i - 1) / 2$ (向下整除)。当索引越界时,代表空结点或结点不存在。
|
||||
具体地,给定索引 $i$ ,那么其左子节点索引为 $2i + 1$ 、右子节点索引为 $2i + 2$ 、父节点索引为 $(i - 1) / 2$ (向下整除)。当索引越界时,代表空节点或节点不存在。
|
||||
|
||||

|
||||
|
||||
@@ -418,7 +418,7 @@
|
||||
|
||||
### 访问堆顶元素
|
||||
|
||||
堆顶元素是二叉树的根结点,即列表首元素。
|
||||
堆顶元素是二叉树的根节点,即列表首元素。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@@ -482,9 +482,9 @@
|
||||
|
||||
### 元素入堆
|
||||
|
||||
给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入结点到根结点这条路径上的各个结点**,该操作被称为「堆化 Heapify」。
|
||||
给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入节点到根节点这条路径上的各个节点**,该操作被称为「堆化 Heapify」。
|
||||
|
||||
考虑从入堆结点开始,**从底至顶执行堆化**。具体地,比较插入结点与其父结点的值,若插入结点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个结点;直至越过根结点时结束,或当遇到无需交换的结点时提前结束。
|
||||
考虑从入堆节点开始,**从底至顶执行堆化**。具体地,比较插入节点与其父节点的值,若插入节点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个节点;直至越过根节点时结束,或当遇到无需交换的节点时提前结束。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -504,7 +504,7 @@
|
||||
=== "<6>"
|
||||

|
||||
|
||||
设结点总数为 $n$ ,则树的高度为 $O(\log n)$ ,易得堆化操作的循环轮数最多为 $O(\log n)$ ,**因而元素入堆操作的时间复杂度为 $O(\log n)$** 。
|
||||
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ ,易得堆化操作的循环轮数最多为 $O(\log n)$ ,**因而元素入堆操作的时间复杂度为 $O(\log n)$** 。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@@ -588,13 +588,13 @@
|
||||
|
||||
### 堆顶元素出堆
|
||||
|
||||
堆顶元素是二叉树根结点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有结点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤:
|
||||
堆顶元素是二叉树根节点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有节点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤:
|
||||
|
||||
1. 交换堆顶元素与堆底元素(即交换根结点与最右叶结点);
|
||||
1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点);
|
||||
2. 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素);
|
||||
3. 从根结点开始,**从顶至底执行堆化**;
|
||||
3. 从根节点开始,**从顶至底执行堆化**;
|
||||
|
||||
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根结点的值与其两个子结点的值,将最大的子结点与根结点执行交换,并循环以上操作,直到越过叶结点时结束,或当遇到无需交换的结点时提前结束。
|
||||
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根节点的值与其两个子节点的值,将最大的子节点与根节点执行交换,并循环以上操作,直到越过叶节点时结束,或当遇到无需交换的节点时提前结束。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||