数据结构--堆

数据结构--堆

  • 概述
  • 堆实现
    • 堆插入,堆移除
  • 堆排序
    • 建堆
    • 排序
  • TOP-K
  • 优先级队列
  • 定时器
  • 中位数

堆的引用场景非常多,比如堆排序,TOP-K,优先级队列等;这里在学习及使用过程中做一个记录;
好记性不如烂笔头

概述

  • 堆是一个完全二叉树(除最后一层,其它层均是满节点,最后一程的节点都靠左);
  • 堆中每个节点的值都必须大于等于(或小于等于)其子树中每个节点的值;(等价于: 堆中每个节点的值都大于等于(或小于等于)其左右子节点的值)
    大于等于 — 大顶堆; 小于等于 — 小顶堆

堆实现

对于完全二叉树,可以通过 数组 来进行存储;可以方便的通过索引来获取到每一个元素;
规律:

当前节点索引: i
左子节点索引: i * 2
右子节点索引: i * 2 + 1
父节点的索引: i / 2

堆插入,堆移除

在我们采用数组实现堆时,明显直接插入到数组最后,是有最小操作的插入方式;插入后,再根据堆结构和其对应父节点进行比较,进行位置调整;
这个过程称之为 堆化(heapify)
堆化分类:

  • 自下而上:
    将新插入的节点同其父节点进行比较,如果不满足关系,则进行交换;
  • 自上而下:
    场景:删除堆顶元素。为了满足完全二叉树,通常删除堆顶元素后,我们将最后一个元素移动到堆顶位置;再自上而下进行比较交换;

堆化复杂度:

  1. 空间上,即为数据交换的过程,因此空间复杂度为 O(1);
  2. 时间上,一个包含 n 个节点的完全二叉树,树的高度不会超过 log ⁡ 2 n \log_2n log2n ;堆化的过程即是顺着节点路径比较交换,所以堆化的时间复杂度跟树的高度成正比,即: O( log ⁡ n \log n logn);

tip: 插入和删除操作主要逻辑就是堆化,因此插入一个元素或删除堆顶元素的时间复杂度也为 O( log ⁡ n \log n logn)

堆排序

堆排序时间复杂度可以做到和归并排序、快速排序、线性排序一样,为O(n log ⁡ n \log n logn);
堆排序整个过程,可分为: 建堆排序

建堆

  1. 假设堆中仅有索引为1的元素,依次从索引为2的元素开始向堆中添加进行堆化,最终组织成堆;
  2. 叶子节点不需要比较, 因此可以从下往上进行比较,即从高度 2 开始到 h;当一个数组的长度为 N 时, 那个最后一个元素父节点的索引一定是 N/2;因此只需要遍历 N/2 个元素即可;参见伪代码:

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=1h+21(h1)+...+2k(hk)+...+2h11
根据 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+(2h2)+2h=2h+1h2
由于 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)
堆排序为原地排序,但是是非稳定的排序算法;

TOP-K

在内存中维护一个小顶堆, 遍历所有数据,当数据大于堆顶元素时,移除堆顶元素并插入当前数据,进行堆化;
最差时间复杂度为: O ( n log ⁡ n ) O(n\log n) O(nlogn)

优先级队列

构建大顶堆,根据优先级进行数据堆化,堆顶元素即为最高优先级;

定时器

构建小顶堆, 堆化数据 v:执行时间点时间-建堆的时间;堆顶元素即为最早执行的任务;
当执行堆顶任务后,重新堆化并计算堆顶元素的值;
定时器在:v - (任务插入时间-建堆时间) 这段时间内,可以什么都不需要做;
可以再每隔固定时间,更新堆的建堆时间,更新时需要重新计算整个堆的值;

中位数

构建一个大顶堆,一个小顶堆;以 中位数为堆顶元素;

  1. 对于奇数n,大顶堆中有 n 2 + 1 \frac n 2+1 2n+1个元素, 小顶堆有 n 2 \frac n 2 2n个元素;
  2. 对于偶数n, 大顶堆,小顶堆中均 n 2 \frac n 2 2n个元素;

当插入一个数时,判断是否大于大顶堆堆顶元素,大于则插入小顶堆,否则插入大顶堆;
再判断大小顶堆是否符合个数要求,不符合则通过移除堆顶元素,平衡两个堆即可;

大顶堆的堆顶元素即为中位数;

你可能感兴趣的:(设计模式及算法,数据结构,算法)