961全部内容链接
优先队列就是一个队列(Queue),但是它与普通的队列不一样的地方在于,它并不一定是先进先出,它需要再结合优先级,来决定谁先出。
比如,操作系统要决定当前CPU要给哪个程序使用,那么它就需要找到一个优先级更高的进程来分配CPU,而这些进程就是放在优先队列中的。操作系统只要选择优先级最高那个进行出队操作就行了。
优先队列的实现方式有很多种,最经典型的实现方式有以下三种:
数据结构 | 入队时间复杂度 | 出队时间复杂度 |
---|---|---|
普通顺序表 | O(1) | O(n) |
有序顺序表 | O(n) | O(1) |
二叉堆 | O(log n) | O(log n) |
二叉堆分为最大堆和最小堆(也可以叫大根堆和小根堆)。最大堆就是说数值越大优先级越高,而最小堆就是数值越小优先级越高。书上使用的最小堆,所以这里也默认堆指的是最小堆。
堆满足以下特定:
如图所示,这棵完全二叉树就是一个最大堆。其中每一个根节点都小于它的所有子孙节点。比如“13小于21,16,24…”,“16小于19,68”。
通过这样的堆,每次出队时,只需要取根节点,然后对最大堆进行调整。入队时,先将其放在最后一个节点,然后调整最大堆。
public class BinaryHeap<AnyType extends Comparable<? super AnyType>> {
public BinaryHeap(int capacity /*堆的大小*/) {
}
private int currentSize;
private AnyType[] array;
public void insert(AnyType x) {
}
// 出队
public AnyType deleteMin() {
}
}
基本上考试会考的也就是入队(insert)和出队(deleteMin)操作。书上给出了其他操作,应该不考,考了也能现写出来,比较简单。
对于一个堆的插入,需要分两部:
具体方式如图:
注:堆与完全二叉树一样,数组0的位置一般不存放数据,为了方便计算。后面提到的计算均按照这个原则进行
该例子为要在该完全二叉树(最小堆)中插入元素14,其中圆圈代表的是元素14。分为如下几步:
代码如下:
public void insert(AnyType x) {
if (currentSize >= array.length - 1) {
// 判断队列是否已满,该句在书上为扩充队列
throw new RuntimeException("队列已满");
}
currentSize++; // 堆元素数++
array[currentSize] = x; // 将x放入堆的最后一个位置
int i = currentSize; // 记录当前x所在的下标
while (i != 1 // i 还没有到达堆顶
&& x.compareTo(array[i / 2]) < 0 // x小于其父节点
) {
// 满足while的两个条件,继续进行上滤操作,即交换x与父节点的位置
AnyType temp = array[i / 2];
array[i / 2] = x;
array[i] = temp;
i = i / 2; // i移动到其父节点的位置
}
}
堆的删除就是出队操作。每次选取堆顶的元素进行出队,出队之后要对整个二叉堆进行调整操作。调整有不同的做法,但必不可少的就是下滤操作。按照书中的过程如下:
需要注意的几点:
代码如下:
public AnyType deleteMin() {
if (currentSize <= 0) {
return null; // 删除失败,二叉堆为空
}
AnyType minResult = array[1];
array[1] = array[currentSize]; // 将堆的最后一个元素换到栈顶
array[currentSize] = null; // 将堆最后一个元素置空,这步骤可以省略。
currentSize--;
// 开始进行下滤操作
percolateDown(1);
return minResult; // 返回最开始的堆顶元素
}
// 下滤操作,这个之所以抽出来,是因为构建堆的时候需要用
private void percolateDown(int hole) {
int i = hole;// 记录当前下滤到的位置
while (i<=currentSize/2) {
// 当下滤到叶子节点时,跳出循环
int child = i*2; // 将child置为左孩子
if (child + 1 <= currentSize // 判断是否有右孩子,防止指针越界
&& array[child].compareTo(array[child + 1]) > 0 // 判断左孩子与右孩子的大小
) {
child ++; //若右孩子比左孩子小,则将child指向右孩子,否则还是左孩子
}
if (array[child].compareTo(array[i]) < 0) {
// 如果当前节点的孩子比它小,则交换当前节点与它的孩子
AnyType temp = array[i]; // 交换当前节点与其孩子节点
array[i] = array[child];
array[child] = temp;
} else {
// 当前节点比它的左右孩子都小,则下滤完成,跳出循环。
break;
}
i = child; // 当前节点向下,指向其孩子节点
}
}
构建堆就是给定一个数组,然后根据该数组,生成一个二叉堆。过程如下:
代码如下:
public BinaryHeap(AnyType[] items) {
array = (AnyType[]) new Comparable[items.length + 100]; // +100是指给堆一些冗余空间。
currentSize = items.length;
// 将给定数组的数据复制到array中
for (int i = 0; i < items.length; i++) {
array[i + 1] = items[i];
}
// 准备工作结束,开始进行Heapify
for (int i = currentSize / 2; i > 0; i--) {
percolateDown(i);
}
}
构建堆的时间复杂度为O(n)
TODO
import java.util.Arrays;
public class BinaryHeap<AnyType extends Comparable<? super AnyType>> {
public BinaryHeap(int capacity /*堆的大小*/) {
currentSize = 0;
array = (AnyType[]) new Comparable[capacity + 1];
}
private int currentSize;
private AnyType[] array;
public BinaryHeap(AnyType[] items) {
array = (AnyType[]) new Comparable[items.length + 100]; // +100是指给堆一些冗余空间。
currentSize = items.length;
// 将给定数组的数据复制到array中
for (int i = 0; i < items.length; i++) {
array[i + 1] = items[i];
}
// 准备工作结束,开始进行Heapify
for (int i = currentSize / 2; i > 0; i--) {
percolateDown(i);
}
}
public void insert(AnyType x) {
if (currentSize >= array.length - 1) {
// 判断队列是否已满,该句在书上为扩充队列
throw new RuntimeException("队列已满");
}
currentSize++; // 堆元素数++
array[currentSize] = x; // 将x放入堆的最后一个位置
int i = currentSize; // 记录当前x所在的下标
while (i != 1 // i 还没有到达堆顶
&& x.compareTo(array[i / 2]) < 0 // x小于其父节点
) {
// 满足while的两个条件,继续进行上滤操作,即交换x与父节点的位置
AnyType temp = array[i / 2];
array[i / 2] = x;
array[i] = temp;
i = i / 2; // i移动到其父节点的位置
}
}
// 出队
public AnyType deleteMin() {
if (currentSize <= 0) {
return null; // 删除失败,二叉堆为空
}
AnyType minResult = array[1];
array[1] = array[currentSize]; // 将堆的最后一个元素换到栈顶
array[currentSize] = null; // 将堆最后一个元素置空,这步骤可以省略。
currentSize--;
// 开始进行下滤操作
percolateDown(1);
return minResult; // 返回最开始的堆顶元素
}
// 下滤操作,这个之所以抽出来,是因为构建堆的时候需要用
private void percolateDown(int hole) {
int i = hole;// 记录当前下滤到的位置
while (i <= currentSize / 2) {
// 当下滤到叶子节点时,跳出循环
int child = i * 2; // 将child置为左孩子
if (child + 1 <= currentSize // 判断是否有右孩子,防止指针越界
&& array[child].compareTo(array[child + 1]) > 0 // 判断左孩子与右孩子的大小
) {
child++; //若右孩子比左孩子小,则将child指向右孩子,否则还是左孩子
}
if (array[child].compareTo(array[i]) < 0) {
// 如果当前节点的孩子比它小,则交换当前节点与它的孩子
AnyType temp = array[i]; // 交换当前节点与其孩子节点
array[i] = array[child];
array[child] = temp;
} else {
// 当前节点比它的左右孩子都小,则下滤完成,跳出循环。
break;
}
i = child; // 当前节点向下,指向其孩子节点
}
}
// 检查堆是否正确
public void checkHeap() {
for (int i = 1; i <= currentSize / 2; i++) {
if (array[i].compareTo(array[i * 2]) > 0) {
throw new RuntimeException("Heap is error!");
}
if (i * 2 + 1 < currentSize && array[i].compareTo(array[i * 2 + 1]) > 0) {
throw new RuntimeException("Heap is error!");
}
}
}
public static void main(String[] args) {
BinaryHeap<Integer> binaryHeap = new BinaryHeap<>(30);
for (int i = 0; i < 20; i++) {
int r = (int) (Math.random() * 99);
System.out.print(r + ", ");
binaryHeap.insert(r);
}
System.out.println();
System.out.println(Arrays.toString(binaryHeap.array));
binaryHeap.checkHeap();
System.out.print("依次删除: ");
for (int i = 0; i < 20; i++) {
System.out.print(binaryHeap.deleteMin() + " ");
binaryHeap.checkHeap();
}
System.out.println();
Integer[] testArray = new Integer[20];
for (int i = 0; i < 20; i++) {
int r = (int) (Math.random() * 99);
testArray[i] = r;
}
BinaryHeap heap2 = new BinaryHeap<>(testArray);
heap2.checkHeap();
System.out.println(Arrays.toString(heap2.array));
}
}