堆详解

1,238 阅读4分钟

完全二叉树

在二叉树中每一层要么是满的,要么是从左到右依次变满。

而堆就是一棵完全二叉树。除了有完全二叉树的特性外,对于堆,任何一个节点为头的子树最大值(或者最小值)为头节点自己。

    9
   / \
  3   6
 / \
1   2 

以上就是一个大根堆

实现

用数组结构实现。比如对于堆存储的数组中是 [9,3,6,1,2],对于任意 i 位置来说,左孩子是2*i+1,右孩子是2*i+2,父节点是 (i-1)/2

基本操作

对于堆来说,有两个基本操作heapInsert(往堆中插入元素)、heapify(调整堆的位置)。因此掌握这两个操作是重点。

场景: 比如对于堆 [5, 1, 4, 0,], insert 3 后。这就不是大根堆了,因此需要调整。

    5
   / \
  1   4
 / \
0   3

heapInsert

1、定义变量 heapSize, 这个变量有两个作用 1、表示堆大小 2、堆插入时下一个数的位置。
2、对于新插入的位置 i,计算 i 的父亲 p = (i-1)/2,如果 arr[i] > arr[p], 交换 i、p 位置,把p的位置赋值给 i。
3、循环执行2,直到 p === 0 或者 arr[i] <= arr[p] 跳出循环

以下代码可以满足 p === 0 或者 arr[i] <= arr[p] 跳出循环的逻辑
while(arr[index] > arr[parseInt((index - 1)/2)]) {}

heapInsert代码

heapify

场景: 比如对于堆 [5, 4, 1, 0, 3],弹出堆顶元素 5 ,把剩余的元素继续调整堆结构。

    5
   / \
  4   1
 / \
0   3

伪代码:

1、把堆顶元素记录下来,以备以后返回,然后把堆中最后一个元素覆盖堆顶,heapSize--。
2、i 和 i 左、右孩子较大一个进行比较,如果孩子大,则把 i 和左右孩子中较大一个进行交换,交换后,i 移动到较大孩子的节点。
3、循环执行2,直到 i 比左右孩子较大的那个小,或者 i > heapSize

heapify代码

堆排序

堆排序就是把 heapInsert 和 heapify 结合起来的一个算法

1、先通过 heapInsert 把一个数组调整成大根堆
2、把堆顶元素和堆最后一个元素交换,然后 heapSize--。根据堆顶进行 heapify 操作。
3、重复执行2,直到 heapSize === 0,说明数组已经排好序

heapSort代码

复杂度分析

时间复杂度

heapInsert是 O(logN), 也要进行 N 次,总共建堆的时间是 O(NlogN)。

heapify的最多是调整树的高度 O(logN),而要进行 N 次,堆中所有元素调整完成时间复杂度是 O(NlogN)

所以 堆排序的时间复杂度是 O(NlogN)

在建堆的过程中,时间复杂度可以缩短成O(N),但在 heapify 操作是不能改变的还是 O(NlogN)。所以时间复杂度还是 O(NlogN)

空间复杂度

排序过程中只用到了某些变量,比如 heapSize 、lc 、rc。

因此空间复杂度是 O(1)

大根堆

任何一个节点为头的子树最大值为头节点自己。

大根堆代码

小根堆

任何一个节点为头的子树最小值为头节点自己。

小根堆代码

如图通过上面小根堆代码一次弹出堆顶元素,是依次变大的。

bitMap

应用堆排序的思想的题目

获取中位数

有一个源源不断吐出整数的数据流,假设你有足够的空间来保存吐出的数。
请设计一个叫medianHolder函数。这个函数可以随时吐出这些数的中位数。
要求
1、如果MedianHolder已经保存了吐出的N个数,那么任意时刻将一个新数加入到MedianHolder的过程,其时间复杂度是 O(logN)
2、随时吐出整数的时间复杂度是 O(1)

这个数组中较大的 N/2 放在小根堆中,较小的 N/2 放到大根堆中,并且在放入的过程中, 如果大小根堆的差值超过1,则通过弹出、加入的方式调整堆。

最后如果整个数据流吐出是奇数时,则把数据比较大的那个堆顶返回;偶数时,则返回两个堆顶元素的平均值。

代码

原文地址 文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。你的star✨、点赞和关注是我持续创作的动力!