最大堆 / 最小堆
1、什么是堆?
堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
- 完全二叉树:若设二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k 层所有的结点都连续集中在最左边,这就是完全二叉树。
- 满二叉树:一棵二叉树的结点要么是叶子结点,要么它有两个子结点(如果一个二叉树的层数为K,且结点总数是(2^k) -1,则它就是满二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
2、优先队列
优先队列和其实是队列的一种
普通队列:先进先出;后进先出
优先队列:出队顺序和入队顺序无关;和优先级相关
我们对队列这种抽象的数据结构做一些限制,创造出优先队列这个概念,我们在实现这个概念时可以使用不同的底层实现
我们可以用堆这样的数据结构实现优先队列这样的逻辑结构
3、最大堆的实现
3.1、最大堆的几个操作
- 建立堆:指定堆空间大小
- 插入元素insert
- 查看元素的个数
- 删除堆顶元素:上浮操作和下沉操作
3.2、实现
我们为什么可以用数组这样的数据结构实现最大堆?
因为一棵节点数量为k的完全二叉树满足这样的性质:
如果将完全二叉树按照层序遍历放入一个长度为k的数组中,则:
- 如果一个节点的数组索引为m,则如果该节点有父节点,父节点的索引为
m / 2
- 如果该节点有孩子节点,则左孩子节点索引为
2 * m
,右孩子为2 * m + 1
如此我们就可以用数组这样简单的数据结构实现了
代码实现:
// 堆中的数据元素必须实现Comparable接口,根据定义的比较规则作为优先级的规则
class MaxPQ<Key extends Comparable<Key>> {
public Key[] pq; // 整体用数组实现,但是索引为0不存储,从索引1开始
private int N = 0; // 表示目前堆的数据占用情况
// 建堆
public MaxPQ(int maxN) { // maxN为堆空间的最大值
pq = (Key[])new Comparable[maxN + 1]; // 因为索引0不存储,所以总共需要maxN+1个空间
}
// 查看当前堆中是否有元素
public boolean isEmpty(){
return N == 0;
}
// 查看堆顶元素
public Key peek(){
return pq[1];
}
// 查看当前堆空间占用几个
public int size(){
return N;
}
// 插入元素
// 首先将新插入的元素放在数组最后,然后执行上浮操作
public void insert(Key v) {
pq[++N] = v;
swim(N);
}
// 删除最大值(堆顶元素)
// 首先将数组最后的元素覆盖堆顶元素,删除最后元素,然后下沉找到合适位置
public Key delMax(){
Key max = pq[1];
exch(1, N--);
pq[N + 1] = null;
sink(1);
return max;
}
// 根据数组索引比较堆中元素大小(优先级)
private boolean less(int i, int j){
return pq[i].compareTo(pq[j]) < 0;
}
// 根据数组索引交换堆中元素
private void exch(int i, int j){
Key t = pq[i];
pq[i] = pq[j];
pq[j] = t;
}
// 上浮,索引为k
// 如果一个节点的优先级大于父节点,就要上浮
// 将该节点和父节点交换,知道不大于父节点或者到堆定
private void swim(int k){
/**
* k/2: 代表父节点的索引
* 如果k不是堆顶元素 并且比它的父节点的值还要大,就要上浮
*/
while (k > 1 && less(k/2, k)){
exch(k, k/2);
k = k / 2;
}
}
// 下沉操作
// 如果一个节点(优先级)不能满足大于两个孩子节点,就要下沉
// 下沉操作需要将该元素和孩子节点中的较大节点交换,直到大于两个子节点或到堆底
private void sink(int k){
/**
* 2*k: 代表左子节点
* 首先判断如果该节点有没有坐子节点,有的话才执行
* 首先j设置成坐子节点的索引,比较坐子节点和右子节点,将j设置为较大的一个
* 然后在该节点的优先级小于较大子节点的优先级的情况下交换,否则退出循环
* 将k设置为交换后的节点,继续循环
*/
while (2 * k <= N){
int j = 2 * k;
if(j < N && less(j, j+1)) j++;
if(!less(k, j)) break;
exch(k, j);
k = j;
}
}
}
4、复杂度分析
很容易看出,弹出 / 插入的时间复杂度都为O(logN)
N为堆的节点数量
堆排序的就是先根据元素集合建立堆,然后不断的弹出堆顶元素,知道堆为空,这样弹出的顺序就是有序的,其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)
级。