学 无 止 境 , 与 君 共 勉 。
相关系列
介绍
在之前的队列篇中,我们介绍了使用数组的方式来实现优先级队列,通过这种方式,尽管删除最高优先级数据的时间复杂度为O(1),但是在插入的时候平均需要移动数组中一半的数据,时间复杂度为O(N)。本文我们讲解堆并以此来实现优先级队列,它的插入、删除时间复杂度都为O(logN)。
特性
- 完全二叉树:除了树最后一层节点不需要是满的,其他每一层的节点从左到右都是满的。
- 最小堆:每一个节点都小于它的子节点
- 最大堆:每一个节点都大于它的子节点
- 通常用一组数组的形式来表示这个树结构
- 无法按顺序遍历节点
堆的数组表示形式
由于堆是一个完全二叉树的关系,树中间是没有空节点的,因此树中每一个节点都可以在一个数组中得到映射。通常我们通过数组的形式去快速定位堆中相应节点的位置。如图所示:
如果某个节点在数组中的索引为X,则满足一下条件:
- 它的父节点在数组中的索引为 (X-1)/2
- 它的左子节点在数组中的索引为 2*X+1
- 它的右子节点在数组中的索引为2*X+2
如上图中节点90对应数组的索引为1,则父节点99所在的索引为(1-1)/2=0,左子节点50所在的索引为(2x1+1)=3,右子节点60所在的索引为(2x1+2)=4
删除最大节点(向下筛选)
本文以最大堆为例子,根节点的数据是最大的,每次删除最大值的时候,只需要删除根节点的值就可以了,即数组对应索引0的位置。但是移除后的树变得不完整了,对应数组的首位没有了,可以通过将数组全部前移一位去调整树结构,但是这种方式效率低,我们采用以下步骤去调整树结构:
- 移除根节点;
- 把最后一个节点(最后一层最右边的节点)移到拿出来临时存储;
- 从根节点向下筛选,把子节点中大的节点和最后一个节点比较;
- 重复上述步骤,直到遇到子节点都小于最后一个节点或者没有子节点为止;
插入节点(向上筛选)
插入操作和删除类似,只是我们从下往上筛选对比,而且父节点只有一个,所以也省去了比较的操作。步骤如下:
- 先将节点插入当前堆的最后一个位置;
- 和父节点对比,如果大于它当前的父节点,父节点的值存入当前位置,将要插入的节点拿出来临时存储;
- 重复上述对比步骤,直到遇到小于父节点或者处于根节点为止;
复制不交换
在对堆的删除、新增操作中,我们是通过将要删除或者要插入的节点放入临时存储中去和其他节点进行比较的。并没有将其放入节点中,每次比较之后进行位置交换操作的,这样做的目的是为了减少复制操作。
如果要将A和B两个节点进行交换我们需要进行3次复制操作:
- 将A复制到临时变量
- 将B复制到A
- 从临时变量复制到B
如上图,如果要将A最终转移到D的位置,通过交换操作需要进行3次交换操作,即9次复制操作。而通过临时变量存储A的方式只需要进行5次复制操作,减少了操作。我们设移动的层次为d,则通过交换操作需要进行3d次复制,通过临时变量的方式,只需要d+2次复制,当树的层次越多,越接近3倍操作
代码实现
public class MaxHeap<E extends Comparable<E>> {
/**
* 队列的最大容量
*/
private int maxSize;
/**
* 当前队列已存在的元素数量
*/
private int items;
/**
* 存储数据的数组
*/
private Object[] heapArray;
public MaxHeap(int maxSize) {
this.maxSize = maxSize;
this.items = 0;
this.heapArray = new Object[maxSize];
}
public boolean isEmpty() {
return items == 0;
}
public boolean isFull() {
return items == maxSize;
}
/**
* 插入:
* 1. 先将节点插入当前堆的最后一个位置;
* 2. 和父节点对比,如果大于它当前的父节点,父节点的值存入当前位置,将要插入的节点拿出来临时存储;
* 3. 重复上述对比步骤,直到遇到小于父节点或者处于根节点为止;
*/
@SuppressWarnings("unchecked")
public boolean insert(E data) {
if (isFull()) {
System.out.println("插入【" + data + "】失败:堆已经满了!!!");
return false;
}
// 空堆,直接插入根节点
if (isEmpty()) {
heapArray[items++] = data;
return true;
}
// 获取插入后最后一位的索引,并将数量+1
int index = items++;
// 放入临时变量
E temp = data;
// 父节点
E parent;
// 小于父节点或者到达根节点
while (index > 0) {
parent = (E) heapArray[(index - 1) / 2];
if (temp.compareTo(parent) < 0) {
break;
}
// 父节点位置下移
heapArray[index] = parent;
index = (index - 1) / 2;
}
heapArray[index] = temp;
return true;
}
/**
* 删除最大节点
* 1. 移除根节点;
* 2. 把最后一个节点(最后一层最右边的节点)移到拿出来临时存储;
* 3. 从根节点向下筛选,把子节点中大的节点和最后一个节点比较;
* 4. 重复上述步骤,直到遇到子节点都小于最后一个节点或者没有子节点为止;
*/
@SuppressWarnings("unchecked")
public E removeMax() {
if (isEmpty()) {
System.out.println("删除失败:堆已经空了!!!");
return null;
}
// 要删除返回的节点
E root = (E) heapArray[0];
// 最后一个节点放入临时变量用来对比
E temp = (E) heapArray[--items];
// 最终存放临时变量的位置
int index = 0;
// 子节点中较大的节点
int largerChildIndex;
E largerData;
/*
* 确保当前节点至少有一个子节点
* 即 2*index - 1 <= 最大索引 (items -1)
* 即 index < items/2
*/
while (index < items / 2) {
largerChildIndex = getLargerChildIndex(index);
largerData = (E) heapArray[largerChildIndex];
if (temp.compareTo(largerData) >= 0) {
break;
}
// 子节点上移
heapArray[index] = largerData;
index = largerChildIndex;
}
heapArray[index] = temp;
return root;
}
/**
* 获取较大子节点的索引
*
* @param curIndex 当前节点的索引
* @return larger index
*/
@SuppressWarnings("unchecked")
private int getLargerChildIndex(int curIndex) {
int rightIndex = 2 * curIndex + 2;
if (rightIndex > items - 1) {
return items;
}
int leftIndex = 2 * curIndex + 1;
E leftChild = (E) heapArray[leftIndex];
E rightChild = (E) heapArray[rightIndex];
if (rightChild.compareTo(leftChild) > 0) {
return rightIndex;
} else {
return leftIndex;
}
}
/**
* 按树形结构打印
*/
public void display() {
System.out.println();
System.out.println("array format: ");
for (Object o : heapArray) {
System.out.print(o + " ");
}
System.out.println();
System.out.println("tree format: ");
System.out.println("========================================================================================");
// 打印空格数
int blanks = 32;
// 每一行节点的个数
int itemsPreRow = 1;
// 当前节点的序号
int column = 0;
// 当前要打印节点的索引
int printIndex = 0;
while (items > 0) {
// 每一行的首个节点
if (column == 0) {
for (int i = 0; i < blanks; i++) {
System.out.print(" ");
}
}
System.out.print(heapArray[printIndex]);
// 所有节点已打印完成
if (++printIndex == items) {
break;
}
// 每一行的所有节点全部打印完成
if (++column == itemsPreRow) {
blanks /= 2;
itemsPreRow *= 2;
column = 0;
// 换行
System.out.println();
} else {
for (int i = 0; i < blanks * 2 - 2; i++) {
System.out.print(" ");
}
}
}
System.out.println();
System.out.println("========================================================================================");
}
}
访问源码
本系列所有代码均上传至Github上,方便大家访问
日常求赞
创作不易,如果各位觉得有用有帮助,求点赞支持