完全二叉树
在二叉树中每一层要么是满的,要么是从左到右依次变满。
堆
而堆就是一棵完全二叉树。除了有完全二叉树的特性外,对于堆,任何一个节点为头的子树最大值(或者最小值)为头节点自己。
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)]) {}
heapify
场景: 比如对于堆 [5, 4, 1, 0, 3],弹出堆顶元素 5 ,把剩余的元素继续调整堆结构。
5
/ \
4 1
/ \
0 3
伪代码:
1、把堆顶元素记录下来,以备以后返回,然后把堆中最后一个元素覆盖堆顶,heapSize--。
2、i 和 i 左、右孩子较大一个进行比较,如果孩子大,则把 i 和左右孩子中较大一个进行交换,交换后,i 移动到较大孩子的节点。
3、循环执行2,直到 i 比左右孩子较大的那个小,或者 i > heapSize
堆排序
堆排序就是把 heapInsert 和 heapify 结合起来的一个算法
1、先通过 heapInsert 把一个数组调整成大根堆
2、把堆顶元素和堆最后一个元素交换,然后 heapSize--。根据堆顶进行 heapify 操作。
3、重复执行2,直到 heapSize === 0,说明数组已经排好序
复杂度分析
时间复杂度
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)。
大根堆
任何一个节点为头的子树最大值为头节点自己。
小根堆
任何一个节点为头的子树最小值为头节点自己。
如图通过上面小根堆代码一次弹出堆顶元素,是依次变大的。
应用堆排序的思想的题目
获取中位数
有一个源源不断吐出整数的数据流,假设你有足够的空间来保存吐出的数。
请设计一个叫medianHolder函数。这个函数可以随时吐出这些数的中位数。
要求
1、如果MedianHolder已经保存了吐出的N个数,那么任意时刻将一个新数加入到MedianHolder的过程,其时间复杂度是 O(logN)
2、随时吐出整数的时间复杂度是 O(1)
这个数组中较大的 N/2 放在小根堆中,较小的 N/2 放到大根堆中,并且在放入的过程中, 如果大小根堆的差值超过1,则通过弹出、加入的方式调整堆。
最后如果整个数据流吐出是奇数时,则把数据比较大的那个堆顶返回;偶数时,则返回两个堆顶元素的平均值。
原文地址 文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。你的star✨、点赞和关注是我持续创作的动力!