This commit is contained in:
krahets
2023-04-10 03:12:02 +08:00
parent 2289822dfd
commit 867ecf6d92
32 changed files with 329 additions and 445 deletions
+1 -1
View File
@@ -162,7 +162,7 @@ comments: true
- 完全二叉树中,设节点总数为 $n$ ,则叶节点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此,在排除叶节点后,需要堆化的节点数量为 $(n - 1)/2$ ,复杂度为 $O(n)$
- 在从顶至底堆化的过程中,每个节点最多堆化到叶节点,因此最大迭代次数为二叉树高度 $O(\log n)$
将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$。**然而,这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的特性**。
将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$ 。**然而,这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的特性**。
接下来我们来进行更为详细的计算。为了减小计算难度,我们假设树是一个“完美二叉树”,该假设不会影响计算结果的正确性。设二叉树(即堆)节点数量为 $n$ ,树高度为 $h$ 。上文提到,**节点堆化最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。
+29 -29
View File
@@ -4,7 +4,7 @@ comments: true
# 8.1.   堆
「堆 Heap」是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型:
「堆 Heap」是一种满足特定条件的完全二叉树,可分为两种类型:
- 「大顶堆 Max Heap」,任意节点的值 $\geq$ 其子节点的值;
- 「小顶堆 Min Heap」,任意节点的值 $\leq$ 其子节点的值;
@@ -13,19 +13,19 @@ comments: true
<p align="center"> Fig. 小顶堆与大顶堆 </p>
## 8.1.1. &nbsp; 堆术语与性质
堆作为完全二叉树的一个特例,具有以下特性:
- 由于堆是完全二叉树,因此最底层节点靠左填充,其它层节点被填满。
- 二叉树的根节点对应「堆顶」,底层最靠右节点对应「堆底」。
- 对于大顶堆 / 小顶堆,堆顶元素(即根节点)的值最大 / 最小
- 最底层节点靠左填充,其他层的节点被填满。
- 我们将二叉树的根节点称为「堆顶」,底层最靠右节点称为「堆底」。
- 对于大顶堆小顶堆,堆顶元素(即根节点)的值分别是最大(最小)的
## 8.1.2. &nbsp; 堆常用操作
## 8.1.1. &nbsp; 堆常用操作
值得说明的是,多编程语言提供的是「优先队列 Priority Queue」,是一种抽象数据结构,**定义为具有出队优先级的队列**
需要指出的是,多编程语言提供的是「优先队列 Priority Queue」,是一种抽象数据结构,定义为具有优先级排序的队列。
而恰好**堆的定义与优先队列的操作逻辑完全吻合**,大顶堆就是一个元素从大到小出队的优先队列。从使用角度看,我们可以将「优先队列」和「堆」理解为等价的数据结构。因此,本文与代码对两者不做特别区分,统一使用「堆」来命名。
实际上**堆通常用作实现优先队列,大顶堆相当于元素从大到小顺序出队的优先队列**。从使用角度看,我们可以将「优先队列」和「堆」看作等价的数据结构。因此,本对两者不做特别区分,统一使用「堆」来命名。
堆的常用操作见下表,方法名需根据编程语言确定。
堆的常用操作见下表,方法名需根据编程语言确定。
<div class="center-table" markdown>
@@ -39,11 +39,11 @@ comments: true
</div>
我们可以直接使用编程语言提供的堆类(或优先队列类)。
在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。
!!! tip
类似于排序“从小到大排列”和“从大到小排列”,“大顶堆”和“小顶堆”可仅通过修改 Comparator 来互相转换。
类似于排序算法中的“从小到大排列”和“从大到小排列”,我们可以通过修改 Comparator 来实现“小顶堆”与“大顶堆”之间的转换。
=== "Java"
@@ -307,23 +307,23 @@ comments: true
```
## 8.1.3. &nbsp; 堆的实现
## 8.1.2. &nbsp; 堆的实现
下文实现的是大顶堆」,若想转换为小顶堆,将所有大小逻辑判断取逆(例如将 $\geq$ 替换为 $\leq$ 即可,有兴趣的同学可自行实现。
下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如将 $\geq$ 替换为 $\leq$ 。感兴趣的读者可以自行实现。
### 堆的存储与表示
在二叉树章节我们学过,「完全二叉树非常适合使用「数组来表示,而堆恰好是一完全二叉树,**因而我们采用数组来存储「堆」**。
我们在二叉树章节中学习到,完全二叉树非常适合数组来表示。由于堆正是一完全二叉树,**我们采用数组来存储**。
**二叉树指针**。使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置**节点指针通过索引映射公式来实现**。
使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置**节点指针通过索引映射公式来实现**。
具体,给定索引 $i$ 那么其左子节点索引为 $2i + 1$ 右子节点索引为 $2i + 2$ 父节点索引为 $(i - 1) / 2$ (向下整)。当索引越界时,表空节点或节点不存在。
具体而言,给定索引 $i$ ,其左子节点索引为 $2i + 1$ 右子节点索引为 $2i + 2$ 父节点索引为 $(i - 1) / 2$(向下整)。当索引越界时,表空节点或节点不存在。
![堆的表示与存储](heap.assets/representation_of_heap.png)
<p align="center"> Fig. 堆的表示与存储 </p>
我们将索引映射公式封装成函数,便后续使用。
我们可以将索引映射公式封装成函数,便后续使用。
=== "Java"
@@ -510,7 +510,7 @@ comments: true
### 访问堆顶元素
堆顶元素二叉树的根节点,即列表首元素。
堆顶元素即为二叉树的根节点,也就是列表的首个元素。
=== "Java"
@@ -601,9 +601,9 @@ comments: true
### 元素入堆
给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其元素,此时堆的成立条件可能已被破坏,**因此需要修复从插入节点到根节点这条路径上的各个节点**操作被称为「堆化 Heapify」。
给定元素 `val` ,我们先将其添加到堆底。添加后,由于 val 可能大于堆中其元素,堆的成立条件可能已被破坏。因此**需要修复从插入节点到根节点路径上的各个节点**这个操作被称为「堆化 Heapify」。
考虑从入堆节点开始,**从底至顶执行堆化**。具体地,比较插入节点与其父节点的值,插入节点更大则将它们交换;并循环以上操作,从底至顶修复堆中的各个节点直至越过根节点时结束,或当遇到无需交换的节点时提前结束。
考虑从入堆节点开始,**从底至顶执行堆化**。具体来说,我们比较插入节点与其父节点的值,如果插入节点更大则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点直至越过根节点遇到无需交换的节点时结束。
=== "<1>"
![元素入堆步骤](heap.assets/heap_push_step1.png)
@@ -623,7 +623,7 @@ comments: true
=== "<6>"
![heap_push_step6](heap.assets/heap_push_step6.png)
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 易得堆化操作的循环轮数最多为 $O(\log n)$ **因而元素入堆操作的时间复杂度为 $O(\log n)$** 。
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ ,**元素入堆操作的时间复杂度为 $O(\log n)$** 。
=== "Java"
@@ -879,13 +879,13 @@ comments: true
### 堆顶元素出堆
堆顶元素是二叉树根节点,即列表首元素如果我们直接将首元素从列表中删除,则二叉树中所有节点都会随之发生移位(索引发生变化,这后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤:
堆顶元素是二叉树根节点,即列表首元素如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引变动,我们采取以下操作步骤:
1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点);
2. 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素);
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素);
3. 从根节点开始,**从顶至底执行堆化**;
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根节点的值与其两个子节点的值,将最大的子节点与根节点执行交换,并循环以上操作,直到越过叶节点时结束,或当遇到无需交换的节点时提前结束。
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换;然后循环执行此操作,直到越过叶节点遇到无需交换的节点时结束。
=== "<1>"
![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png)
@@ -917,7 +917,7 @@ comments: true
=== "<10>"
![heap_pop_step10](heap.assets/heap_pop_step10.png)
与元素入堆操作似,**堆顶元素出堆操作的时间复杂度为 $O(\log n)$**
与元素入堆操作似,堆顶元素出堆操作的时间复杂度为 $O(\log n)$ 。
=== "Java"
@@ -1275,8 +1275,8 @@ comments: true
}
```
## 8.1.4. &nbsp; 堆常见应用
## 8.1.3. &nbsp; 堆常见应用
- **优先队列**。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 $O(\log n)$ ,建队操作为 $O(n)$ 非常高效。
- **堆排序**给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并小堆的长度即可
- **获取最大的 $k$ 个元素**。这既是一经典算法题目,也是一种常见应用,例如选热度前 10 的新闻作为微博热搜,选取前 10 销量的商品等。
- **优先队列**:堆通常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 $O(\log n)$ 建队操作为 $O(n)$ 这些操作都非常高效。
- **堆排序**给定一组数据,我们可以用它们建立一个堆,然后依次将所有元素弹出,从而得到一个有序序列。当然,堆排序的实现方法并不需要弹出元素,而是每轮将堆顶元素交换至数组尾部并小堆的长度。
- **获取最大的 $k$ 个元素**:这是一经典算法问题,同时也是一种典型应用,例如选热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。
+6 -6
View File
@@ -4,9 +4,9 @@ comments: true
# 8.3. &nbsp; 小结
- 堆是一棵限定条件下的完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素最大(小)。
- 优先队列定义为一种具有出队优先级的队列。堆是实现优先队列的最常用数据结构
- 堆的常用操作对应时间复杂度元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 访问堆顶元素 $O(1)$ 等。
- 完全二叉树非常适合用数组表示,因此我们一般用数组来存储堆。
- 堆化操作用于修复堆的特性,在入堆和出堆操作中都会使用到。
- 输入 $n$ 个元素并建堆的时间复杂度可以优化至 $O(n)$ ,非常高效。
- 堆是一棵完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素最大(小)
- 优先队列定义具有出队优先级的队列,通常使用堆来实现
- 堆的常用操作及其对应时间复杂度包括:元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 访问堆顶元素 $O(1)$ 等。
- 完全二叉树非常适合用数组表示,因此我们通常使用数组来存储堆。
- 堆化操作用于维护堆的性质,在入堆和出堆操作中都会用到。
- 输入 $n$ 个元素并建堆的时间复杂度可以优化至 $O(n)$ ,非常高效。