堆的引用场景非常多,比如堆排序,TOP-K,优先级队列等;这里在学习及使用过程中做一个记录;
好记性不如烂笔头
对于完全二叉树,可以通过 数组
来进行存储;可以方便的通过索引来获取到每一个元素;
规律:
当前节点索引: i
左子节点索引: i * 2
右子节点索引: i * 2 + 1
父节点的索引: i / 2
在我们采用数组实现堆时,明显直接插入到数组最后,是有最小操作的插入方式;插入后,再根据堆结构和其对应父节点进行比较,进行位置调整;
这个过程称之为 堆化(heapify)
堆化分类:
堆化复杂度:
tip: 插入和删除操作主要逻辑就是堆化,因此插入一个元素或删除堆顶元素的时间复杂度也为 O( log n \log n logn)
堆排序时间复杂度可以做到和归并排序、快速排序、线性排序一样,为O(n log n \log n logn);
堆排序整个过程,可分为: 建堆 和 排序
private static void buildHeap(int[] a, int n) {
for (int i = n/2; i >= 1; --i) {
heapify(a, n, i);
}
}
private static void heapify(int[] a, int n, int i) {
while (true) {
int maxPos = i;
if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;
if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;
if (maxPos == i) break;
swap(a, i, maxPos);
i = maxPos;
}
}
对于方法二的时间复杂度:
非叶子节点均需要进行堆化操作;每个节点堆化的过程中,需要比较和交换的节点个数,跟这个节点的高度 k 成正比。因此有:
S = 1 ∗ h + 2 1 ∗ ( h − 1 ) + . . . + 2 k ∗ ( h − k ) + . . . + 2 h − 1 ∗ 1 S = 1*h+2^1*(h-1)+...+2^k*(h-k)+...+2^{h-1}*1 S=1∗h+21∗(h−1)+...+2k∗(h−k)+...+2h−1∗1
根据 S = 2S - S, 错位相减有:
S = − h + ( 2 h − 2 ) + 2 h = 2 h + 1 − h − 2 S = -h+(2^h-2)+2^h = 2^{h+1}-h-2 S=−h+(2h−2)+2h=2h+1−h−2
由于 h = log 2 n h = \log_2n h=log2n,因此有 S = O ( n ) S = O(n) S=O(n)
对于大顶堆
,只需要将堆顶元素放到下表为n
的位置,再对前n-1
个元素进行堆化操作即可达到排序的目的;
// 参考伪代码
// n表示数据的个数,数组a中的数据从下标1到n的位置。
public static void sort(int[] a, int n) {
buildHeap(a, n);
int k = n;
while (k > 1) {
swap(a, 1, k);
--k;
heapify(a, k, 1);
}
}
复杂度分析:
建堆的时间复杂度为 O ( n ) O(n) O(n), 排序的时间复杂度为 O ( n log n ) O(n\log n) O(nlogn);
因此堆排序的整体时间复杂度为 O ( n log n ) O(n\log n) O(nlogn);
堆排序为原地排序,但是是非稳定的排序算法;
在内存中维护一个小顶堆
, 遍历所有数据,当数据大于堆顶元素时,移除堆顶元素并插入当前数据,进行堆化;
最差时间复杂度为: O ( n log n ) O(n\log n) O(nlogn)
构建大顶堆
,根据优先级进行数据堆化,堆顶元素即为最高优先级;
构建小顶堆
, 堆化数据 v:执行时间点时间-建堆的时间
;堆顶元素即为最早执行的任务;
当执行堆顶任务后,重新堆化并计算堆顶元素的值;
定时器在:v - (任务插入时间-建堆时间)
这段时间内,可以什么都不需要做;
可以再每隔固定时间,更新堆的建堆时间
,更新时需要重新计算整个堆的值;
构建一个大顶堆
,一个小顶堆
;以 中位数
为堆顶元素;
当插入一个数时,判断是否大于大顶堆堆顶元素,大于则插入小顶堆,否则插入大顶堆;
再判断大小顶堆是否符合个数要求,不符合则通过移除堆顶元素,平衡两个堆即可;
大顶堆的堆顶元素即为中位数;