此次用近万字的讲解带你干掉堆!





0. 前言

你们好,我是多选参数的程序锅,一个正在捣鼓操做系统、学数据结构和算法以及 Java 的失业人员。最近忙着搞论文,还有刷刷 LeetCode 上的题,推文的事被耽误了一下,可是并无忘记要发推文,虽迟但到吧。node

整篇文章的主要知识提纲如图所示,本篇相关的代码均可以从 https://github.com/DawnGuoDev/algorithm 获取,另外,该仓库除了包含了基础的数据结构和算法实现以外,还会有数据结构和算法的笔记、LeetCode 刷题记录(多种解法、Java 实现) 、一些优质书籍整理。git

1. 基本概念

堆是一种特殊的树。只要知足如下两个条件就是一个堆。github

  • 堆是一个彻底二叉树。既然是彻底二叉树,那么使用数组存储的方式将会很方便。
  • 堆中的每一个节点的值都必须大于等于(或小于等于)其子节点的值。对于大于等于子节点的堆又被称为“大顶堆”;小于等于子节点的堆又被称为“小顶堆”。

以下图所示,一、2 是大顶堆,3 是小顶堆,4 不是堆。同时,咱们能够看出,对于同一组数据,能够构建出不一样形态的堆。web

2. 堆的存储

知足堆的一个要求是”堆是一个彻底二叉树“,而彻底二叉树比较适合用数组存储,一是节省空间,二是经过数组的下标就能够找到父节点、左右子节点(数组下标最好从 1 开始)。本篇讲的例子,及其代码的实现都将从数组下标为 1 的地方开始。算法

下面是一个用数组存储堆的例子,假如从下标 1 开始存储堆的数据,那么下标为 i 的节点的左子节点在数组中的下标为 2*i,右子节点在数组中的下标为 2*i+1,父节点在数组中的下标为 i/2。假设从下标 0 开始存储的话,下标为 i 的节点的左子节点的下标为 2*i+1,右子节点的下标为 2*i+2,父节点的下标为 (i-1)/2api

3. 堆的操做

堆最核心的操做是分别往堆中插入一个元素以及删除堆顶元素。为何这边只提到删除堆顶元素?由于删除堆的其余元素是毫无心义,堆顶元素通常是最大或者最小值,因此删除堆顶元素才有意思。数组

另外,在往堆中插入一个元素或者删除堆顶元素以后,须要确保知足堆的两个特性。而将不符合两个特性的“堆”调整成符合两个特性堆的过程被称为堆化(heapify)。堆化有两种方式:从上往下堆化和从下往上堆化。二者的区别在于调整的顺序,从上往下堆化是指从堆顶元素开始向下调整使其最终符合两个特性,而从下往上则相反。在插入和删除的操做中会具体看到这两种方式。缓存

下面的阐述都将以大顶堆为例,小顶堆同理。微信

3.1. 往堆中插入一个元素

插入元素时,将新插入的元素放到堆的最后比较方便。此时,采用从下往上的堆化方法比较合适,整个从下往上的堆化过程就是向上不断跟父节点对比,而后交换。如图所示,咱们往一棵现成的堆中插入了一个元素 22。那么此时的“堆”不符合堆的两个特性了。那么新插入的节点先与父节点进行比较,不知足父子节点的应有的大小(大顶堆中,父节点的值应该大于子节点)则互换两个节点。互换以后的父节点和它的父节点不知足大小关系的话就继续交换。重复这个过程,直至调整过程当中的父子节点都知足大小关系为止。数据结构

咱们将这个从下到上的堆化过程,实现为以下代码段所示

public void insert(int data) {
if (this.count >= this.capacity) {
return;
}
++this.count;
this.heap[this.count] = data;
heapifyFromBottom();
}

public void heapifyFromBottom() {
int i = this.count;
while (i/2 > 0 && this.heap[i] > this.heap[i/2]) {
swap(i, i/2);
i = i/2;
}
}

3.2. 删除堆顶元素

从堆的第二点特性“堆中的每一个节点的值都必须大于等于(或小于等于)其子节点的值”能够推出堆中最大(或最小)的值存储在堆顶元素中(大顶堆堆顶则是最大值)。所以删除堆顶元素是有意义的,而删除堆中的其余元素是没有意义的。

那么删除堆顶元素以后,整个堆就不符合堆的两个条件了。此时,正确的方式是把最后一个节点放到堆顶处,而后从上而下依次比较父子节点的大小,若是不知足大小关系则进行交换,直至父子节点之间知足大小关系为止。这种方法就是从上往下的堆化方法

如图所示,假如将堆顶的元素 33 删除以后,那么将最后一个节点的元素 12 放到堆顶处。而后 12 与 27 进行比较,由于不知足大顶堆的要求,12 与 27 进行交换。以此类推,最终调整后的堆知足了所要求的两个特性。

咱们将上述从上而下的堆化过程实现为以下图所示

public void delete () {
if (this.count <= 0) {
return;
}

this.heap[1] = this.heap[this.count--];

heapifyFromTop(1);
}

public void heapifyFromTop(int begin) {
while (true) {
int i = begin; // i 是节点及其左右子节点中较大值的那个节点的下标

/* 就是在节点及其左右子节点中选择一个最大的值,与节点所处的位置进行;
可是,须要注意的是假如这个值正好是节点自己,那么直接退出循环;
不然须要进行交换,而后从交换以后的节点开始继续堆化 */

if (begin * 2 <= this.count && this.heap[begin] < this.heap[2 * begin]) {
i = 2 * begin;
}

if ((2 * begin + 1) <= this.count && this.heap[i] < this.heap[2 * begin + 1]) {
i = 2 * begin + 1;
}

if (i == begin) {
break;
}

swap(begin, i);

begin = i;
}
}

3.3. 时间复杂度

堆化的过程是顺着节点的路径进行比较交换的,那么比较交换这一组操做的次数是跟树的层次有关的。对于一棵含 n 个节点的彻底二叉树来讲,树的层次不会超过  。所以,整个堆化过程的时间复杂度是 O(logn)。那么,插入一个元素和删除堆顶元素的时间复杂度都是 O(logn)

4. 堆排序

借助堆这种数据结构实现的排序被称为堆排序。堆排序是一种原地的、时间复杂度为 O(nlogn) 且不稳定的排序算法。整个堆排序分为建堆和排序两个步骤。

4.1. 堆排序过程

4.1.1. 建堆

首先是将待排序数组创建成一个堆,秉着能不借助额外数组则不借助的原则,咱们能够直接在原数组上直接操做。这样,建堆有两个方法:

  • 第一种方法相似于上述堆的操做中“往堆中插入一个元素”的思想。刚开始的时候假设堆中只有一个元素,也就是下标为 1 的元素。而后,将下标为 2 的元素插入堆中,并对堆进行调整。以此类推,将下标从 2 到 n 的元素依次插入到堆中。这种建堆方式至关于将待排序数组分红“堆区”和“待插入堆区”。

    如图所示,咱们将对待排序数据 七、五、1九、八、4 进行建堆(大顶堆)。能够看到初始化堆就一个元素 7。以后将指针移到下标为 2 的位置,将 5 这个元素添加到堆中并从下往上进行堆化。以后,再将指针移到下标为 3 的位置,将 19 这个元素添加到堆中并从下往上进行堆化。依次类推。

  • 第二种方法是将整个待排序数组都当成一个“堆”,可是这个“堆”不必定知足堆的两个条件,所以咱们须要对其进行总体调整。那么,调整的时候是从数组的最后开始,依次往前调整。调整的时候,只须要调整该节点及其子树知足堆的要求,而且是从上往下的方式进行调整。因为,叶子节点没有子树,因此叶子节点不必调整,咱们只须要从第一个非叶子节点开始调整(这边的第一是从后往前数的第一个)。那么第一个非叶子节点的下标为 n/2,所以咱们只须要对 n/2 到 1 的数组元素进行从上往下堆化便可(下标从 n/2 到 1 的数组元素所在节点都是非叶子节点,下标从 n/2+1 到 n 的数组元素所在节点都是叶子节点)。

    如图所示,咱们将对待排序数据 七、五、1九、八、四、一、20、1三、16 进行建堆(大顶堆)。能够看到整个过程是从 8 这个元素开始进行堆化。在对 8 进行堆化的时候,仅对 8 及其子树进行堆化。在对 5 进行堆化的时候,仅对 5 及其子树进行堆化。


咱们将第二种思路实现成以下代码段所示:

public void buildHeap(int[] datas, int len) {
this.heap = datas;
this.capacity = len - 1;
this.count = len - 1;
for (int i = this.count/2; i >=1; i--) {
heapifyFromTop(i);
}
}

public void heapifyFromTop(int begin) {
while (true) {
int i = begin; // i 是节点及其左右子节点中较大值的那个节点的下标

/* 就是在节点及其左右子节点中选择一个最大的值,与节点所处的位置进行;
可是,须要注意的是假如这个值正好是节点自己,那么直接退出循环;
不然须要进行交换,而后从交换以后的节点开始继续堆化 */

if (begin * 2 <= this.count && this.heap[begin] < this.heap[2 * begin]) {
i = 2 * begin;
}

if ((2 * begin + 1) <= this.count && this.heap[i] < this.heap[2 * begin + 1]) {
i = 2 * begin + 1;
}

if (i == begin) {
break;
}

swap(begin, i);

begin = i;
}
}

为何下标从 n/2 到 1 的数组元素所在节点都是非叶子节点,下标从 n/2+1 到 n 的数组元素所在节点都是叶子节点?这个算是彻底二叉树的一个特性。严格的证实暂时没有,不严谨的证实仍是有的。这里采用反证法,假如 n/2 + 1 不是叶子节点,那么它的左子节点下标应该为 n+2,可是整个彻底二叉树最大的节点的下标为 n。因此 n/2 + 1 不是叶子节点不成立,即 n/2 + 1 是叶子节点。那么同理可证 n/2 + 1 到 n 也是如此。而对于下标为 n/2 的节点来讲,它的左子节点有的话下标应该为 n,n 在数组中有元素,所以 n/2 有左子节点,即 n/2 不是叶子节点。同理可证 1 到 n/2 都不是叶子节点。

4.1.2. 排序

建完堆(大顶堆)以后,接下去的步骤是排序。那么具体该怎么实现排序呢?

此时,咱们能够发现,堆顶的元素是最大的,即数组中的第一个元素是最大的。实现排序的过程大体以下:咱们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。此时堆顶元素不是最大,所以须要进行堆化。采用从上而下的堆化方法(参考删除堆顶元素的堆化方法),将剩下的 n-1 个数据构建成堆。最后一个数据由于已知是最大了,因此不用参与堆化了。n-1 个数据构建成堆以后,再将堆顶的元素(下标为 1 的元素)和下标为 n-1 的元素进行交换。一直重复这个过程,直至堆中只剩一个元素,此时排序工做完成。如图所示,这是整个过程的示意图。

下面将这个过程使用 Java 实现,以下图所示

public void heapSort() {
while (this.count > 1) {
swap(this.count, 1);
this.count--;
heapifyFromTop(1);
}
}

4.2. 算法分析

4.2.1. 时间复杂度

堆排序的时间复杂度是由建堆和排序两个步骤的时间复杂度叠加而成。

  • 建堆的时间复杂度

在采用第二方式建堆时,从粗略的角度来看,每一个节点进行堆化的时间复杂度是 O(logn),那么 n/2 个节点堆化的总时间复杂度为 O(nlogn)。可是这此时粗略的计算,更加精确的计算结果不是 O(nlogn),而是 O(n)

由于叶子节点不须要进行堆化,因此须要堆化的节点从倒数第二层开始。每一个节点须要堆化的过程当中,须要比较和交换的次数,跟这个节点的高度 k 成正比。那么全部节点的高度之和,就是全部节点堆化的时间复杂度。假设堆顶节点对应的高度为 h ,那么整个节点对应的高度如图所示(以满二叉树为例,最后一层叶子节点不考虑)。

那么将每一个非叶子节点的高度求和为

求解这个公式可将两边同时乘以 2 获得 S2,

而后再减去 S1,从而就获得 S1

因为

因此最终时间复杂度为 O(2n-logn),也就是 O(n)。

  • 排序的时间复杂度

排序过程当中,咱们须要进行 (n-1) 次堆化,每次堆化的时间复杂度是 O(logn),那么排序阶段的时间复杂度为 O(nlogn)。

  • 总的时间复杂度

那么,整个总的时间复杂度为 O(nlogn)

不对建堆过程的时间复杂度进行精确计算,也就是建堆以 O(nlogn) 的时间复杂度算的话,那么总的时间复杂度仍是 O(nlogn)。

4.2.2. 稳定与否

堆排序不是稳定的排序算法。由于在排序阶段,存在将堆的最后一个节点跟堆顶点进行互换的操做,因此有可能会改变相同数据的原始相对顺序。好比下面这样一组待排序 20、1六、1三、13 ,在排序时,第二个 13 会跟 20 交换,从而更换了两个 13 的相对顺序。

4.2.3. 是否原地

堆排序是原地排序算法,由于堆排序的过程当中的两个步骤中都只须要极个别临时存储空间。

4.3. 堆排序总结

在实际开发中,为何快速排序要比堆排序性能要好?

  1. 对于一样的数据,在排序过程当中,堆排序算法的数据交换次数要多于快速排序

    对于基于比较的排序算法来讲,整个排序过程就是由比较和交换这两个操做组成。快速排序中,交换的次数不会比逆序度多。可是堆排序的过程,第一步是建堆,这个过程存在大量的比较交换操做,而且颇有可能会打乱数据原有的相对前后顺序,致使原数据的有序度下降。好比,在对一组已经按从小到大的顺序排列的数据进行堆排序时,那么建堆过程会将这组数据构建成大顶堆,而这一操做将会让数据变得更加无序。而采用快速排序的方法时,只须要比较而不须要交换。

    最直接的方式就是作个试验看一下,对交换次数进行统计。

  2. 堆排序的访问方式没有快速排序友好

    快速排序来讲,数据是顺序访问的。而堆排序,数据是跳着访问的。访问的数据量如何很大的话,那么堆排序可能对 CPU 缓存不太友好。

5. 堆的其余应用

上面介绍了堆的理论和堆排序,可是堆这种数据结构除了用在堆排序以外,还有其余几个很是重要的应用:优先级队列、求 Top K 问题以及求中位数问题。

5.1. 优先级队列

优先级队列跟普通队列全部不一样。普通队列最大的特性是先进先出,而优先级队列则按照优先级来,优先级高的先出队。咱们能够发现,优先级队列和堆有共通之处。堆顶元素都是最大或者最小的,那么取堆顶元素就相似于优先级队列的出队操做。所以,优先级队列最直接、最高效的实现方式是采用堆来实现。

不少语言也都有提供优先级队列的实现,好比 Java 的 PriorityQueue,C++ 的 priority_queue 等。固然,优先级队列也有不少应用场景,这边举两个简单的例子来看一下优先级队列是怎么用的。

5.1.1. 合并有序小文件

假设咱们须要将 100 个存储着有序的单词的文件合并成一个有序的大文件。按照以前讲过的方法,咱们可使用数组的方式,也就是从这个 100 个文件中,各取第一个单词。以后,根据字典序进行比较,将字典序最小的那个单词放入合并后的大文件,并从数组中删除。而后,从删去单词的那个小文件中再取一个单词,将其放入数组。以后重复上述操做,直至 100 个文件的单词都被合并到最终的有序的大文件中。使用数组这种方式,每次取字典序最小的那个单词时都须要遍历整个数组。显然,数组的方式很不高效。

那么咱们可使用优先级队列,由于咱们每次须要的都是当前字典序最小的那个单词,这跟优先级队列的特性很类似。所以,咱们能够维护一个容量为 100 的小顶堆,每次取小顶堆的堆顶单词,而后将其放入合入的大文件中,以后再从小文件中取新的单词放入堆中,并进行堆化。重复这个过程,最终将 100 个文件的单词数据全都合并到大文件中。那么采用堆的方式以后,每次取字典序最小单词的时间主要就是堆化的时间,而堆化的时间复杂度是 O(logn),这边 n 是 100。显然这个时间比使用数组的方式高效多了。

5.1.2. 高性能定时器

假设有一个定时器,定时器维护了不少任务。每一个任务都设定了一个要触发执行的时间点。普通的方法可能就是采用轮询的方式。每隔一段时间轮询一次(好比 1 秒),查看是否有任务到达设定的执行时间。若是到达了,就拿出来执行。然而,这种作法比较低效。一是可能有些任务执行还要好久的时间,那么前面不少次的扫描将是徒劳的;二是当任务列表很大的时候,每次这么扫描是很费时的。

那么咱们照样可使用优先级队列。咱们按照任务设定的执行时间构建小顶堆,堆顶元素是任务设定的执行时间距离如今最近的任务,也就是应该最早被执行的任务。当咱们拿到堆顶任务的执行时间点以后,与如今的时间进行相减,从而获得一个时间间隔 T1 。接下去,咱们就能够不用每隔 1 秒进行扫描,而是在 T1 秒以后,直接取堆顶的任务来执行。而且,对剩下的任务进行堆化,再计算新的堆顶任务距离如今的时间间隔 T2,将其做为执行下一个任务须要等待的时间,以此类推。采用这种方式后,性能相比每隔 1 秒的轮询方式提升了许多。

5.2. 求 Top K

求 Top K 的问题能够分为两类,一类是针对静态数据集合,也就至关于这个数据集合事先肯定了,不会再变了。另外一类是针对动态数据集合,也就是说数据事先并不彻底肯定,会有数据不断的加入到集合中。下面针对这两类分别进行阐述,求 Top K 大的问题为例。

  • 针对静态数据,使用堆来求 Top K 的方法以下。

    咱们须要维护一个大小为 K 的小顶堆。一开始的时候,咱们能够拿数组前 K 个元素先将这个堆填满,也就至关于用前 K 个元素先构建一个小顶堆。以后,顺序遍历数组剩下的元素。若是,遍历到的元素大于堆顶元素,则将原堆顶元素替换为遍历到的元素,而后丛上而下的方式进行堆化。若是比堆顶元素小,则不作处理,继续遍历。当遍历完成以后,堆中的元素则是前 Top K 大的数据了。

    对前 k 个数据构建堆时,时间复杂度为 O(k),那么假设剩下的 n-k 个数据在遍历以后都须要加入堆并进行堆化,那么这个的时间复杂度为 O((n-k)logk)。总的时间复杂度应该为 O((n-k)logk + k)。简单粗暴点来讲,假如在对前 k 个数据构建堆时,是按照“往堆中插入一个元素”的方式的话,那么时间复杂度也能够算做 O(nlogk)。

  • 针对动态数据,使用堆来求 Top K 的方法以下。

    一样,咱们须要维护一个大小为 K 的小顶堆。一开始的时候,当堆中数据不满 K 个的时候,每当插入一个数据以后,就将其放入堆中。当堆中数据满 K 个以后,若是有新的数据被添加到集合中时,那么先与堆顶元素进行比较。若是大于堆顶元素,则将堆顶元素替换为新数据,而后进行堆化。若是小于堆顶元素,则不作处理。这样,不管何时查询时堆中的元素始终是 Top K 大的。

    固然还有另外一种方法是,在每次查询的时候再针对新的数据集合求 Top K 问题,采用针对静态数据的方式。

    我的以为这种方式和另外一种方式,要具体问题具体分析。好比查询时间比较短,前面第一种的实时方式比较合适。查询时间比较长,而且新数据增长特别快,那么第二种方式比较合适。

快速排序也能求 Top K 的问题,可是快速排序更加适合于静态数据,若是数据集合一直在变更,那堆的方式会更适合一点。

另外在 Top K 的问题上,快排和堆这两种方式,堆会更胜一筹。

5.3. 求中位数及各类百分位的数据

中位数是按顺序排列的一组数据中居于中间位置的数。若是数据的个数是奇数,那么中位数的位置为 n/2+1(数据是从 1 开始编号,n 是最后一个数据的编号)。若是数据的个数是偶数,那么中位数的位置为 n/2 或者 n/2 +1(同上),通常状况下是取这两个数的平均值做为中位数,固然也能够取 n/2 位置的数据做为中位数。下面咱们选择这两个数的平均值做为中位数为例。

  • 针对一组静态数据,求中位树的最好方式就是先排序,而后再取中位数。假若有查询中位数的操做时,能够将第一次求得中位数的值缓存下来或者中位数的位置缓存下来。

  • 针对动态数据集合时,集合不断地变更,中位数也不断的变更。那么,再用先排序再求中位数的方式,效率就不高了。为此,咱们可使用堆这种数据结构,而且维护两个堆,一个大顶堆,一个小顶堆。大顶堆存储前一半的数据,小顶堆存储后一半的数据。大顶堆中的数据都比小顶堆中的数据小,也就是大顶堆的堆顶数据小于等于小顶堆的堆顶数据(有点相似沙漏)。

    如图所示,假如 n 是奇数,那么 n/2+1 个数据全都存储在大顶堆中,剩下的 n/2个数据存储在小顶堆中。这样,大顶堆的顶堆数据就是中位数(固然 n/2 个数据全都存储在大顶堆,剩下的 n/2 + 1 个数据存储在小顶堆中也行,那么小顶堆的顶堆数据就是中位数)。假如 n 是偶数,那么前 n/2 个数据全都存储在大顶堆中,剩下的 n/2 个数据存储在小顶堆中。那么大顶堆的堆顶数据和小顶堆的堆顶数据求平均就是中位数了。

    那么当插入新的数据时,若是这个数据比大顶堆的数据小,那么则将其插入大顶堆。反之,则将其插入小顶堆。同时,插入新的数据以后须要确保两个堆的数据数量比例按照约定来,即插入以后若是数据数量为偶数,那么两个堆的数量须要相等;若是数据数量为奇数,那么大顶堆要比小顶堆多一个数据。假如插入数据以后,两个堆的数据数量不符合约定了,那么则须要移动数据使其知足约定,移动的数据通常都是堆顶元素。

    使用这种方法,插入数据的时候会涉及到堆化,时间复杂度为 O(logn),可是在求中位数的时候,只须要 O(1)。所以,我的以为跟上述求 Top K 的相似。若是查询中位数很频繁,那么动态方式的方式很 nice 了;假如查询中位数不频繁,那么静态的方式可能会更好。

另外,动态数据集合使用两个堆来求中位数的方法也能够很好地用于求其余百分位的数据。求中位数其实就至关于求 50% 处的数据。好比,当求 90% 处的数据时。咱们能够 90% 的数据保存在大顶堆中,10 % 的数据保存在小顶堆中。一样的,插入数据以后也要维护两个堆的比例,而且插入的时间复杂度为 O(logn),而查询的时间复杂度为 O(1)。

这边必定须要两个堆吗?我以为是须要的,好比在查找 90% 的数据时,虽然咱们都只须要取大顶堆的堆顶元素。可是,假如咱们只维护一个大顶堆。某次插入数据时,这个数据没被插入到大顶堆中,结果致使大顶堆和剩下的元素不符合比例了(好比大顶堆比应有的少了一个数据,而小顶堆比应有的多了一个数据)。那么咱们须要从剩下的数据中找出在剩下的数据中最小的那个数据插入大顶堆。此时,假如剩下的数据直接使用小顶堆的方式来表示,那么只须要取堆顶元素便可,很方便。这个使用堆的方式和数组的方式,在合并有序小文件的时候已经作过比较了。因此须要两个堆!

6. 总结

  • 堆是一种很重要的数据结构。堆知足两个特性,一是彻底二叉树,二是任意节点都比其子节点大(或者小)。任意节点都比其子节点大的叫作大顶堆,不然叫作小顶堆。因为堆是一个彻底二叉树,因此堆使用数组的方式存储会更加合适。所以,咱们对堆的操做通常都是在基于数组完成的。

  • 堆常见的操做是插入元素和删除堆顶元素(注意:删除堆中的其余元素是没有意义的)。另外,插入和删除元素以后大部分状况下都是须要进行堆化的,堆化的时间复杂度是 O(logn),所以这两个操做的时间复杂度都是 O(logn)。

  • 堆最多见的应用是堆排序。使用堆排序对一组数据集合进行排序时,分为两步骤:①先是建堆。建堆就是将这组数据结合创建成一个堆,这里能够不用开辟新的内存空间而基于原数据集合直接创建堆。基于原数据集合直接创建堆能够采用两种方式,一种是从头日后开始遍历数组,这种方式相似于往堆中插入数据;另外一种则是从后往前,从最后一个非叶子节点开始对该非叶子及其子树进行从上往下的堆化。②再是排序。因为堆顶的元素是最值,所以先将堆顶元素和最后一个元素交换,而后将堆中的数量减一(至关于把位于最后位置的堆顶元素给删除了)。以后再对剩下的数据进行堆化。堆化完成以后,再取堆顶元素,再交换,以此类推。最终实现排序。

    堆还有其余几种常见的应用。①优先级队列。优先级队列中,优先级最高的会先出队,这跟堆的本质是同样的。那么优先级队列可用于多个有序小文件的合并,同理也适合于多个有序数组的合并。②求 Tok K 问题。Top K 问题也可使用快速排序的方式实现求解。可是,整体来看堆更适合用于求 Top K 问题,尤为针对动态数据集合。堆求 Top K 主要是维护一个容量为 K 的小顶堆,当遍历到的数据比堆顶元素大则加入堆,反之什么也不作。③求中位数或者各类百分位的数据。此时,咱们应该维护两个堆,一个大顶堆,一个小顶堆。大顶堆中的元素都比小顶堆中的元素小。两个堆中的数据比例要符合所求的百分位状况。好比中位数,就两个堆中的数据比例要占 50%。好比 90% 位的数据,那么小顶堆中的数据占 90%,大顶堆中的数据占 10%。那么,小顶堆的堆顶元素就是 90% 位的数据。另外,当两个堆的数据占比不符合约定时,须要进行数据的移动。

    对应用在求 Top K 和中位数时,发现针对动态数据集合效果会更显著。由此,咱们能够想到,堆这种数据结构可能更适合一些动态数据集合的场景。


不甘于「本该如此」,多选参数 值得关注




本文分享自微信公众号 - 多选参数(zhouxintalk)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。