关于堆结构从以下几个方面总结关于此数据结构的问题:
- 生产中的常见问题
- 堆的定义
- 堆的基本操作
- 堆排序
- 堆在生产中的应用
生产中的常见问题
- 优先队列的应用场景很广,它是如何实现的呢?
- 如何求解TopK问题
- TP99是生产中的一个非常重要的指标,如何快速计算
堆的定义
- 堆是一颗完全二叉树,这样实现的堆也被称为二叉堆
- 堆中节点的值都大于等于(或小于等于)其子节点的值,堆中如果节点值都大于等于其子节点的值,我们称这个数据结构就是大顶堆,反之如果都小于等于其子节点的值,那么就称为小顶堆。
(完全二叉树就是它的叶子节点都是在最后一层,并且这些叶子节点都是靠左排序的)
在上图当中1、2是属于大顶堆,3是属于小顶堆。4不属于大顶堆和小顶堆,因为在叶子节点当中不是靠左排序的。
可以注意的是无论是在大顶堆还是在小顶堆当中,节点的值都是大于左右子节点的值,并且左右子节点的值是没有特殊的规定的。(即未规定左右节点的排列方式)
问题:堆的底层该如何表示呢?
因为完全二叉树可以用数组来表示,又因为堆是一颗完全二叉树,所以堆也可以使用数组来表示。
如图示:给完全二叉树从上到下从左到右进行编号排序,则对于任意一个节点来说,很容易得知如果它在数组的位置为i,则它的左右子节点就在数组的2i和2i+1,通过这种方式可以定位到树种的每一个节点。
堆的基本操作
堆有两个基本的操作,构建堆(往堆中插入元素)与删除堆顶元素。
- 往堆中插入元素
往堆中插入元素,往往是在叶子节点(最底层)先插入节点,再进行调整,直到满足堆的特点。(堆中节点值都大于等于/小于等于其子节点的值),将这个过程称为堆化(heaplify)
public class Heap {
private int[] arr; //堆是完全二叉树
private int capacity; //堆中能存储的最大元素数量
private int n; //当前堆中的元素
public static void swap(int[] arr,int i,int j) {
int k = arr[i];
arr[i] = arr[j];
arr[j] = k;
}
public Heap(int count) {
capacity = count;
arr = new int[capacity+1];
n = 0;
}
public void insert(int value) {
if(n > capacity) {
return; //超过了堆的大小就不可以插入元素了
}
n++;
//先将元素插入到队尾中
arr[n] = value;
int i = n;
//这里的判断条件就是如果插入的元素大于它的父节点,那么这个元素就需要上移
while(i/2 > 0 && arr[i] > arr[i/2]) {
swap(arr,i,i/2);
i = i / 2;
}
}
}
这种调整方式是先将元素插入到堆的最后,然后自下而上不断进行调整比较子节点和父节点的值,称为由下而上的堆化
时间复杂度就是树的高度O(logn)
- 删除堆顶元素
假设操作的堆是大顶堆,则删除堆顶元素后,要找到原堆中第二大的元素以填补顶堆元素,而第二大的元素无疑就是在根节点的左右节点当中,假设是左节点,则用左节点来填补堆顶元素后,左节点就空了。之后的过程就是进行不断地循环。
但是,在调整后,在最终调整堆当中,出现了数组空洞,对应数组如下:
解决方案:用最后一个元素覆盖堆顶元素,然后再自上而下调整堆,让其满足大顶堆的要求,即可解决数组空洞的问题。
问题:Java默认的数组排序(Array.sort())底层也是用的是快速排序,时间复杂度和快速排序一样快,为什么堆排序在实际生产当中不受欢迎
主要是以下的两个原因:
- 快速排序在递归排序过程中,都是拿pivot与相邻元素进行比较,会使用到计算机中一个非常重要的原理:局部性原理。
什么是局部性原理?可以简单理解为CPU读取到某个数据的时候,它认为这个数据附近相邻的数据也有很大概率会被用到,所以干脆将被读取到数据的附近数据也一起加载到cache当中,这样下次还需要进行读取操作的时候,就直接从cache里拿数据就可以了(无需再从内存当中进行读写操作了)。如果数据量大的话,就可以有极大的性能提升了。
堆排序无法利用局部性原理,因为在堆化的过程当中,需要不断比较节点与其左右子节点的大小,左右子节点也需要比较其左右子节点。
如图示:在节点2自上而下的堆化过程中,其要遍历的数组有4,5,9,10…中的元素,这些元素不是相邻元素,无法利用局部性原理进行提升性能
- 我们知道堆排序的一个重要步骤是将堆顶元素进行移除,重新进行堆化,每次堆化都会导致大量的元素比较,这也是堆排序性能较差的一个原因。
- 堆排序不是稳定排序,因为堆化开始前需要将首位和末尾元素进行交换,如果两元素值一样,那么就可能会改变他们的相对顺序。