所谓优先级队列,就是在出队列的时候,不在遵循先进先出的规则,而是先出优先级最高的数据。堆这种数据结构实际上是一颗完全二叉树进行调整得到的。
堆的概念
把一组数据集合K中的所有元素k0、k1、k2、k3……按照完全二叉树的顺序存储到一维数组当中,并且满足ki <= k2i+1且ki <= k2i+2(或者ki >= k2i+1且ki >= k2i+2),则称为小堆或小根堆(大堆或大根堆)。也就是说堆中任何一个结点总是不大于(或不小于)它的父节点。
![](https://img-blog.csdnimg.cn/img_convert/e84a3881e16f860fcef118ab3210e549.png)
逻辑结构
![](https://img-blog.csdnimg.cn/img_convert/5a11637bf8fe3592e6e2b70059ae684d.png)
存储结构
堆属于完全二叉树,存储方式为顺序存储。
堆的模拟实现
建堆
堆的模拟实现首先需要建堆,建堆可以通过向下调整的方式实现,以建大根堆为例:
首先需要父结点的值同时大于左孩子和右孩子,对于左右孩子,又分别是一个大根堆,那么向下调整的思路为:将父结点同左右孩子结点值较大的相比较,如果父结点的值小于值较大的孩子结点,将二者进行交换,交换完的结点也需要向下调整。
![](https://img-blog.csdnimg.cn/img_convert/02b561ed51e1c96569bf3f673e69b384.png)
向下调整
public void createHeap(int[] array) {
for (int parent = (usedSize - 2) / 2; parent >= 0; parent--) {
shiftDown(parent, usedSize);
}
}
/**
*
* @param root 是每棵子树的根节点的下标
* @param len 是每棵子树调整结束的结束条件
*/
private void shiftDown(int root,int len) {
int maxChild = root * 2 + 1;
while (maxChild < len) {
if (maxChild + 1 < len && elem[maxChild] < elem[maxChild + 1]) {
maxChild++;
}
if (elem[maxChild] > elem[root]) {
swap(elem, maxChild, root);
root = maxChild;
maxChild = root * 2 + 1;
}else {
break;
}
}
}
向下调整建堆的时间复杂度:O(N)
由于每个结点都需要进行向下调整,而每个结点向下调整的时间复杂的为树的高度,譬如上图:第一层有一个结点,它向下调整的时间复杂度为树的高度,即第一层为:1 * (4 - 1) (树的高度为4),第二层有两个结点:2 * (4 - 2),第三层:4 * (4 - 3),第四层最多会有8个结点,因此时间复杂度为:8 * (4 - 4)
那么对于一颗高度为h的满二叉树来说,其时间复杂度为:
T(n) = 2^0 * (h-1) + 2^1 * (h-2) + 2^2 * (h-3) + …… + 2^(h-2) * 1 + 2^(h-1) * 0
利用错位相减法可得T(n) = 2^h - 1 - h
一颗高度为h的满二叉树与结点个数n的关系为:n = 2^h - 1; -> h = log2(n + 1)
即T(n) = n - log2(n + 1) - 1≈n
入队和出队
入队和出队,不会改变堆的性质,一个大根堆(小根堆),在入队或出队后,仍然属于大根堆(小根堆)。
入队(插入):把入队元素放入队尾,由于其已经是大根堆,只需调整新入队元素和其父结点,若新入队元素大于父结点值,交换二者的值,父结点由于值改变,需要依次向上比较,倘若小于父结点,则说明已经满足大根堆,调整结束:
public void offer(int val) {
//堆为数组,需要判断数组是否放满元素
if (isFull()) {
//扩容
elem = Arrays.copyOf(elem, 2 * elem.length);
}
elem[usedSize++] = val;
shiftUp(usedSize - 1);
}
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (elem[child] > elem[parent]) {
swap(elem, child, parent);
child = parent;
parent = (child - 1) / 2;
}else {
break;
}
}
}
出队(删除):每次删除只能删除队头元素,由于没有办法直接删除队头元素,做法是将队头元素与队尾元素交换,自然地舍弃队尾元素,再将新的队头元素向下调整即可:
public void pollHeap() {
if (isEmpty()) {
return;
}
swap(elem, 0, elem.length - 1);
usedSize--;//访问不到即删除
shiftDown(0, usedSize);
}
获取堆顶元素比较简单,直接返回队头元素即可:
public int peekHeap() {
return usedSize == 0 ? null : elem[0];
}