目录
堆
堆可以是一个完全二叉树,这样实现的堆也被称为二叉堆。完全二叉树,它的叶子节点都在最后一层,并且这些叶子节点都是靠左排序的。
堆中节点的值都 >=(或 <=)其子节点的值,堆中如果节点的值都 >= 其子节点的值,我们把它称为大顶堆,如果都 <= 其子节点的值,我们将其称为小顶堆。
从堆的特点可知,下图中 1、2 是大顶堆,3 是小顶堆, 4 不是堆(不是完全二叉树)。
从上图也可以看到,一组数据如果表示成大顶堆或小顶堆,可以有不同的表示方式,因为它只要求节点值 >=(或 <=)子节点值,并未规定左右子节点的排列方式。
堆的底层是如何表示的呢,从以上堆的介绍中我们知道堆是一颗完全二叉树,而完全二叉树可以用数组表示:
如上图示,给完全二叉树按从上到下、从左到右编号,则对于任意一个节点来说,很容易得知如果它在数组中的位置为 i,则它的左右子节点在数组中的位置为 2i、2i + 1,通过这种方式可以定位到树中的每一个节点,从而串起整颗树。
一般对于二叉树来说每个节点是要存储左右子节点的指针,而由于完全二叉树的特点(叶子节点都在最后一层,并且这些叶子节点都是靠左排序的),用数组来表示它再合适不过,用数组来存储的好处在于不需要存指向左右节点的指针,在这颗树很大的情况下能省下很多空间。
堆的应用
-
优先级队列:我们知道队列都是先进先出的,而在优先级队列中,元素被赋予了权重的概念,权重高的元素优先执行,执行完之后下次再执行权重第二高的元素。显然用堆来实现优先级队列再合适不过了,只要用一个大顶堆来实现优先级队列即可,当权重最高的队列执行完毕,将其移除(相当于删除堆顶),再选出优先级第二高的元素(堆化让其符合大顶堆的条件)。
-
求 TopK 问题:求出 n 个元素中前 K 个最大/最小的元素。
-
求 TP99 问题:TP99 指的是在一个时间段内(如5分钟),统计某个接口(或方法)每次调用所消耗的时间,并将这些时间按从小到大的顺序进行排序,取第99%的那个值作为 TP99 值,举个例子, 假设这个方法在 5 分钟内调用消耗时间为从 1 s 到 100 s 共 100 个数,则其 TP99 为 99,这个值为啥重要呢,对于某个接口来说,这个值越低,代表 99% 的请求都是非常快的,说明这个接口性能很好,反之,就说明这个接口需要改进。
有人可能会说以上的这些应用貌似用快排或其他排序也能实现,没错,确实能实现,但是我们需要注意到,在静态数据下用快排确实没问题,但在动态数据上,如果每插入/删除一个元素对所有的元素进行快排,其实效率不是很高,由于要快排要全量排序,时间复杂度是 O(nlog n),而堆排序就非常适合这种对于动态数据的排序,对于每个新添加的动态数据,将其插入到堆中,然后进行堆化,时间复杂度只有 O(logK)
综上,堆是一种非常重要的数据结构,在对动态数据进行排序时性能很高,优先级队列底层也是普遍采用堆来管理。
堆的基本操作
堆有两个基本的操作:
- 构建堆(往堆中插入元素)
- 删除堆顶元素
往堆中插入元素
往堆中插入元素后(如下图示),我们需要继续满足堆的特性,所以需要不断调整元素的位置直到满足堆的特点为止(堆中节点的值都 >= 或 <= 其子节点的值),我们把这种调整元素以让其满足堆特点的过程称为堆化(Heapify)。
由于上图中的堆是个大顶堆,所以我们需要调整节点以让其符合大顶堆的特点:不断的比较子节点与父节点,如果子节点大于父节点,则交换,不断重复此过程,直到子节点小于其父节点。来看下上图插入节点 11 后的堆化过程:
这种调整方式是先把元素插到堆的最后,然后自下而上不断比较子节点与父节点的值,我们称之为由下而上的堆化。时间复杂度就是树的高度,所以为 O(logn)。
删除堆顶元素
由于堆的特点,所以其根节点(堆项)要么是所有节点中最大,要么是所有节点中最小的,当删除堆顶元素后,也需要调整子节点,以让其满足堆(大顶堆或小顶堆)的条件。
假设我们要操作的堆是大顶堆,则删除堆顶元素后,要找到原堆中第二大的元素以填补堆顶元素,而第二大的元素无疑是在根节点的左右子节点上,假设是左节点,则用左节点填补堆顶元素之后,左节点空了,此时需要从左节点的左右节点中找到两者的较大值填补左节点。不断迭代此过程,直到调整完毕,调整过程如下图示:
但是这么调整后,问题来了,如上图所示,在最终调整后的堆中,出现了数组空洞,对应的数组如下:
我们可使用最后一个元素覆盖堆顶元素,然后再自上而下地调整堆,让其满足大顶堆的要求,这样即可解决数组空洞的问题。
时间复杂度和插入堆中元素一样,也是树的高度,所以为 O(logn)。
堆的排序
用堆怎么实现排序?我们知道在大顶堆中,根节点是所有节点中最大的,于是我们有如下思路:
假设待排序元素个数为 n(假设其存在数组中),对这组数据构建一个大顶堆,删除大顶堆的元素(将其与数组的最后一个元素进行交换),再对剩余的 n-1 个元素构建大顶堆,再将堆顶元素删除(将其与数组的倒数第二个元素交换),再对剩余的 n-2 个元素构建大顶堆。不断重复此过程,这样最终得到的排序一定是从小到大排列的,堆排序过程如下图所示:
从以上的步骤中可以看到,重要的步骤就两步,建堆(堆化,构建大顶堆)与排序。
参考文章
https://mp.weixin.qq.com/s/lk_lQJw8QSVQDqYLvRkfrQ
来源:oschina
链接:https://my.oschina.net/u/4293376/blog/4266755