文章目录
-
-
-
- 一、二叉堆
- 二、相关操作
-
- 2.1 插入节点
- 2.2 删除节点
- 2.3 构建二叉堆
- 三、优先队列
- 四、堆排序
- 五、其余补充
-
-
学习自书籍——《小灰的算法之旅》~
一、二叉堆
二叉堆本质上是一种完全二叉树,分为大根堆和小根堆两类:
- 大根堆:任何一个父结点的值都大于或等于它左、右孩子结点的值
- 小根堆:任何一个父结点的值都小于或等于它左、右孩子结点的值
二叉堆的根结点叫做堆顶,因此,大根堆的堆顶是整个堆中的最大元素、小根堆的堆顶是整个堆中的最小元素。
应用:堆排序、优先队列。
二、相关操作
2.1 插入节点
当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。紧接着,需要做一系列“上浮”操作:(以大根堆为例)将最后一个位置的结点依次与它的父节点进行比较,如果大于父节点,则和父节点交换位置,继续这个操作,直到到达堆顶位置。
用到以下二叉树的性质:
- 假设父节点的下标是
,那么它的左孩子下标就是parent
,右孩子下标就是2\*parent+1
2*\parent+2
- 假设孩子节点的下标是
,那么它的父节点下标就是child
(child - 1)/2
以优先队列的“上浮”操作代码为例(其中size表示此时队列中的元素个数):
private void upAdjust() {
int childIndex = size - 1;
int parentIndex = (childIndex - 1) / 2;
int temp = array[childIndex];//使用中间变量,可以做到单向覆盖,循环结束后将其存入交换后的最终位置即可。
while (childIndex > 0 && array[childIndex] > array[parentIndex]) {
array[childIndex] = array[parentIndex];
childIndex = parentIndex;
parentIndex = (childIndex - 1) / 2;
}
array[childIndex] = temp;
}
2.2 删除节点
二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。需要以下几步:
- 删除堆顶节点;
- 将最后一个节点临时补充到堆顶;
- “下沉”操作(和插入时的“上浮”操作相反):让暂存在堆顶位置的节点和他的左右孩子依次比较,如果左右孩子节点中最小的一个比其小,则让堆顶节点下沉,不断重复这个过程。
- 由此,二叉堆重新得到了调整。
以优先队列的“下沉”操作代码为例(其中size表示此时队列中的元素个数):
private void downAdjust() {
int parentIndex = 0;
int childIndex = 1;
int temp = array[parentIndex];
while (childIndex <= size - 1) {
if (array[2 * parentIndex + 2] > array[2 * parentIndex + 1]) {
childIndex++;
}
if (array[parentIndex] >= array[childIndex]) {
break;
}
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = 2 * parentIndex + 1;
}
array[parentIndex] = temp;
}
2.3 构建二叉堆
构建二叉堆就是把一个无序的完全二叉树调整为二叉堆,本质是让所有非叶节点依次”下沉“。
首先从最后一个非叶节点开始,如果总节点数为
size
,那么其下标就是
(size-2)/2
,紧接着依次
-1
继续进行“下沉”操作…
经过多次比较和“下沉”操作,最终每一节点都小于他的左、右孩子节点,构建完成。
public static void buildHeap(int[] array) {
for (int i = (array.length - 2) / 2; i >= 0; i--) {
downAdjust(array, i, array.length);
}
}
三、优先队列
队列:先进先出(FIFO)。
优先队列分为两种:
- 最大优先队列:无论入队顺序如何,都是当前最大元素优先出队
- 最小优先队列:无论入队顺序如何,都是当前最小元素优先出队
要实现这个需求,利用线性数据结构也可以实现,但是时间复杂度较高。
因此,可以使用二叉堆实现:入队即插入、出队即删除。
import java.util.Arrays;
/**
* @author 刘星宇
* @version 1.0
* @date 2021/4/24 14:29
*/
public class PriorityQueue {
private int[] array;
private int size;
public PriorityQueue(int capcity) {
this.array = new int[capcity];
}
public void resize() {
int newSize = this.size * 2;
this.array = Arrays.copyOf(array, newSize);
}
public void enQueue(int key) {
if (size > array.length) {
resize();
}
array[size++] = key;
upAdjust();
}
public int deQueue() throws Exception {
if (size <= 0) {
throw new Exception("该队列为空!");
}
int res = array[0];
array[0] = array[size - 1];
size--;
downAdjust();
return res;
}
private void upAdjust() {
int childIndex = size - 1;
int parentIndex = (childIndex - 1) / 2;
int temp = array[childIndex];
while (childIndex > 0 && array[childIndex] > array[parentIndex]) {
array[childIndex] = array[parentIndex];
childIndex = parentIndex;
parentIndex = (childIndex - 1) / 2;
}
array[childIndex] = temp;
}
private void downAdjust() {
int parentIndex = 0;
int childIndex = 1;
int temp = array[parentIndex];
while (childIndex <= size - 1) {
if (array[2 * parentIndex + 2] > array[2 * parentIndex + 1]) {
childIndex++;
}
if (array[parentIndex] >= array[childIndex]) {
break;
}
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = 2 * parentIndex + 1;
}
array[parentIndex] = temp;
}
public static void main(String[] args) throws Exception {
PriorityQueue priorityQueue = new PriorityQueue(32);
priorityQueue.enQueue(3);
priorityQueue.enQueue(5);
priorityQueue.enQueue(10);
priorityQueue.enQueue(7);
priorityQueue.enQueue(6);
priorityQueue.enQueue(9);
priorityQueue.enQueue(-1);
System.out.println(Arrays.toString(priorityQueue.array));
System.out.println("出队元素:" + priorityQueue.deQueue());
System.out.println("出队元素:" + priorityQueue.deQueue());
System.out.println("出队元素:" + priorityQueue.deQueue());
}
}
四、堆排序
二叉堆的构建、删除、自我调整等基本操作,正是实现堆排序的基础。
由于二叉堆的特性,每一次删除旧堆项,调整后的新堆项都是大小仅次于旧堆项的节点。那么只要反复删除堆顶,反复调整二叉堆,所得到的集合就会成为一个有序集合。
算法步骤:
- 把无序数组构建成二叉堆。需要从小到大排序,则构成大根堆;需要从大到小排序,则构成小根堆。
- 循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆项。
其中大部分操作和上述类似,区别仅在步骤2:
public void buildHeap(int[] array) {
for (int i = (array.length - 2) / 2; i >= 0; i--) {
downAdjust(array, i, array.length);
}
System.out.println(Arrays.toString(array));
//循环删除堆顶元素,移到集合尾部,调整堆产生新堆项
for (int i = array.length - 1 ; i>0;i--){
int temp = array[i];
array[i] = array[0];
array[0] = temp;
//下沉,调整大根堆
downAdjust(array, 0, i);
}
}
也可以使用优先队列依次出队实现堆排序。
五、其余补充
- 二叉堆的插入和删除操作时间复杂度是O(logn),但构建堆的时间复杂度是O(n);
- 二叉堆虽然是一个完全二叉树,但他的存储方式不是链式存储,而是顺序存储;
- 优先队列入队和出队的时间复杂度都是O(logn);
- 堆排序时间复杂度:O(nlogn)、空间复杂度O(1);
- 堆排序是不稳定排序;