mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-30 11:57:13 +00:00
build
This commit is contained in:
@@ -206,9 +206,9 @@ comments: true
|
||||
|
||||

|
||||
|
||||
<p align="center"> 图:完美二叉树的各层节点数量 </p>
|
||||
<p align="center"> 图 8-5 完美二叉树的各层节点数量 </p>
|
||||
|
||||
如上图所示,**节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。
|
||||
如图 8-5 所示,**节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。
|
||||
|
||||
$$
|
||||
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
|
||||
|
||||
+15
-15
@@ -4,14 +4,14 @@ comments: true
|
||||
|
||||
# 8.1 堆
|
||||
|
||||
「堆 heap」是一种满足特定条件的完全二叉树,主要可分为下图所示的两种类型:
|
||||
「堆 heap」是一种满足特定条件的完全二叉树,主要可分为图 8-1 所示的两种类型:
|
||||
|
||||
- 「大顶堆 max heap」:任意节点的值 $\geq$ 其子节点的值。
|
||||
- 「小顶堆 min heap」:任意节点的值 $\leq$ 其子节点的值。
|
||||
|
||||

|
||||
|
||||
<p align="center"> 图:小顶堆与大顶堆 </p>
|
||||
<p align="center"> 图 8-1 小顶堆与大顶堆 </p>
|
||||
|
||||
堆作为完全二叉树的一个特例,具有以下特性:
|
||||
|
||||
@@ -25,9 +25,9 @@ comments: true
|
||||
|
||||
实际上,**堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将“优先队列”和“堆”看作等价的数据结构。因此,本书对两者不做特别区分,统一使用“堆“来命名。
|
||||
|
||||
堆的常用操作见下表,方法名需要根据编程语言来确定。
|
||||
堆的常用操作见表 8-1 ,方法名需要根据编程语言来确定。
|
||||
|
||||
<p align="center"> 表:堆的操作效率 </p>
|
||||
<p align="center"> 表 8-1 堆的操作效率 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -68,11 +68,11 @@ comments: true
|
||||
|
||||
/* 堆顶元素出堆 */
|
||||
// 出堆元素会形成一个从大到小的序列
|
||||
peek = heap.poll(); // 5
|
||||
peek = heap.poll(); // 4
|
||||
peek = heap.poll(); // 3
|
||||
peek = heap.poll(); // 2
|
||||
peek = heap.poll(); // 1
|
||||
peek = maxHeap.poll(); // 5
|
||||
peek = maxHeap.poll(); // 4
|
||||
peek = maxHeap.poll(); // 3
|
||||
peek = maxHeap.poll(); // 2
|
||||
peek = maxHeap.poll(); // 1
|
||||
|
||||
/* 获取堆大小 */
|
||||
int size = maxHeap.size();
|
||||
@@ -331,11 +331,11 @@ comments: true
|
||||
|
||||
当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。**节点指针通过索引映射公式来实现**。
|
||||
|
||||
如下图所示,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下取整)。当索引越界时,表示空节点或节点不存在。
|
||||
如图 8-2 所示,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下取整)。当索引越界时,表示空节点或节点不存在。
|
||||
|
||||

|
||||
|
||||
<p align="center"> 图:堆的表示与存储 </p>
|
||||
<p align="center"> 图 8-2 堆的表示与存储 </p>
|
||||
|
||||
我们可以将索引映射公式封装成函数,方便后续使用。
|
||||
|
||||
@@ -681,7 +681,7 @@ comments: true
|
||||
|
||||
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 heapify」。
|
||||
|
||||
考虑从入堆节点开始,**从底至顶执行堆化**。如下图所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。
|
||||
考虑从入堆节点开始,**从底至顶执行堆化**。如图 8-3 所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -710,7 +710,7 @@ comments: true
|
||||
=== "<9>"
|
||||

|
||||
|
||||
<p align="center"> 图:元素入堆步骤 </p>
|
||||
<p align="center"> 图 8-3 元素入堆步骤 </p>
|
||||
|
||||
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ ,**元素入堆操作的时间复杂度为 $O(\log n)$** 。
|
||||
|
||||
@@ -1058,7 +1058,7 @@ comments: true
|
||||
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。
|
||||
3. 从根节点开始,**从顶至底执行堆化**。
|
||||
|
||||
如下图所示,**“从顶至底堆化”的操作方向与“从底至顶堆化”相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。
|
||||
如图 8-4 所示,**“从顶至底堆化”的操作方向与“从底至顶堆化”相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -1090,7 +1090,7 @@ comments: true
|
||||
=== "<10>"
|
||||

|
||||
|
||||
<p align="center"> 图:堆顶元素出堆步骤 </p>
|
||||
<p align="center"> 图 8-4 堆顶元素出堆步骤 </p>
|
||||
|
||||
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$ 。
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ comments: true
|
||||
|
||||
## 8.3.1 方法一:遍历选择
|
||||
|
||||
我们可以进行下图所示的 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\dots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。
|
||||
我们可以进行图 8-6 所示的 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\dots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。
|
||||
|
||||
此方法只适用于 $k \ll n$ 的情况,因为当 $k$ 与 $n$ 比较接近时,其时间复杂度趋向于 $O(n^2)$ ,非常耗时。
|
||||
|
||||

|
||||
|
||||
<p align="center"> 图:遍历寻找最大的 k 个元素 </p>
|
||||
<p align="center"> 图 8-6 遍历寻找最大的 k 个元素 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -26,17 +26,17 @@ comments: true
|
||||
|
||||
## 8.3.2 方法二:排序
|
||||
|
||||
如下图所示,我们可以先对数组 `nums` 进行排序,再返回最右边的 $k$ 个元素,时间复杂度为 $O(n \log n)$ 。
|
||||
如图 8-7 所示,我们可以先对数组 `nums` 进行排序,再返回最右边的 $k$ 个元素,时间复杂度为 $O(n \log n)$ 。
|
||||
|
||||
显然,该方法“超额”完成任务了,因为我们只需要找出最大的 $k$ 个元素即可,而不需要排序其他元素。
|
||||
|
||||

|
||||
|
||||
<p align="center"> 图:排序寻找最大的 k 个元素 </p>
|
||||
<p align="center"> 图 8-7 排序寻找最大的 k 个元素 </p>
|
||||
|
||||
## 8.3.3 方法三:堆
|
||||
|
||||
我们可以基于堆更加高效地解决 Top-K 问题,流程如下图所示。
|
||||
我们可以基于堆更加高效地解决 Top-K 问题,流程如图 8-8 所示。
|
||||
|
||||
1. 初始化一个小顶堆,其堆顶元素最小。
|
||||
2. 先将数组的前 $k$ 个元素依次入堆。
|
||||
@@ -70,7 +70,7 @@ comments: true
|
||||
=== "<9>"
|
||||

|
||||
|
||||
<p align="center"> 图:基于堆寻找最大的 k 个元素 </p>
|
||||
<p align="center"> 图 8-8 基于堆寻找最大的 k 个元素 </p>
|
||||
|
||||
总共执行了 $n$ 轮入堆和出堆,堆的最大长度为 $k$ ,因此时间复杂度为 $O(n \log k)$ 。该方法的效率很高,当 $k$ 较小时,时间复杂度趋向 $O(n)$ ;当 $k$ 较大时,时间复杂度不会超过 $O(n \log n)$ 。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user