1. 优先级队列
1.1 概念
优先级队列是一种特殊的队列,其中每个元素都有一个优先级。在优先级队列中,具有最高优先级的元素首先出列/(移除)。这与普通队列不同,普通队列是先进先出(FIFO)的,而优先级队列则是按照优先级来确定元素的出队顺序。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
2. 优先级队列的模拟实现
2.1 堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
2.2 堆的性质
-
堆中某个节点的值总是不大于或不小于其父节点的值
-
堆总是一棵完全二叉树
每一颗子树 根节点都是小于孩子结点的(小根堆)
每一棵子树 根节点都是大于孩子节点的(大根堆)
2.3 堆的存储方式
堆是一棵完全二叉树,因此可以用层序遍历的规则采用顺序的方式来高效存储。
注意: 对于非完全二叉树,则不适合采用顺序的方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。如下图所示:
将元素存储到数组中后,可以根据二叉树的性质对树进行还原。假设 i 为结点在数组中的下标,则有:
- 如果 i 为0,则 i 表示的结点为根节点,否则 i 结点的双亲结点为 (i - 1)/ 2
- 如果2 * i + 1 小于结点个数,则结点 i 的左孩子的下标为 2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于结点个数,则结点 i 的右孩子下标为 2 * i + 2,否则没有右孩子
2.4 堆的创建
2.4.1 堆的向下调整
把数组变成大根堆有2种方式:
1. 把整个数组看成一棵树,从最后一棵子树开始 向下调整。
2. 一个元素一个元素的进行插入。
2.4.2 大根堆的创建
1. 左右孩子找最大值 和 根节点大小进行比较
2. 交换完成后,需要继续向下执行,直到下标child的指向 >= 数组的长度
堆的初始化
public int[] elem; //一个数组
public int usedSize; //元素个数
public TestHep(){
this.elem = new int[10];
}
//初始化elem数组的
public void initElem(int[] array){
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
创建大根堆
//创建大根堆
public void createBigHeap(){
for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) { //从树最下面开始循环
siftDown(parent,usedSize); //调节树的父亲结点是谁(变化的),还有结束结点是谁
}
}
private void siftDown(int parent,int end){
int child = 2*parent+1; //树的根节点以0为下标
while(child < end){ //小于数组长度说明没调完
if(child+1 < usedSize && elem[child] < elem[child+1]){
//child+1小于usedsize说明没有越界
//判断左孩子如果比右孩子小的话,child就要指向右孩子,因为下一步要用最大的孩子结点和父亲结点交换
child++;
}
//程序执行到这一步就说明child一定是左右孩子最大值的下标
if(elem[child]>elem[parent]) { //child和parent找出最大值进行交换
//交换
swap(child,parent);
parent = child;
child = 2*parent+1;
}else{
break; //不用交换
}
}
}
private void swap(int i,int j){ //交换的方法
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
有代码可知,创建小根堆的话,把代码里面的大小于号修改即可
2.4.3 堆元素的删除(堆顶)
- 将堆栈元素和最后一个元素交换
- 将堆中有效数据减少一个
- 对堆顶元素向下调整
//堆删除元素
public int poll(){
int tmp = elem[0];
swap(0,usedSize-1); //堆顶元素和最后一个元素交换
usedSize--; //usedSize减一,元素还保留在数组,下次元素插入的时候直接覆盖
siftDown(0,usedSize); //最后一个元素在堆顶,直接向下调整堆顶元素即可
return tmp;
}
private void siftDown(int parent,int end){
int child = 2*parent+1; //树的根节点以0为下标
while(child < end){ //小于数组长度说明没调完
if(child+1 < usedSize && elem[child] < elem[child+1]){ //child+1小于usedsize说明没有越界
//判断左孩子如果比右孩子小的话,child就要指向右孩子,因为下一步要用最大的孩子结点和父亲结点交换
child++;
}
//程序执行到这一步就说明child一定是左右孩子最大值的下标
if(elem[child]>elem[parent]) { //child和parent找出最大值进行交换
//交换
swap(child,parent);
parent = child;
child = 2*parent+1;
}else{
break; //不用交换
}
}
}
2.4.4 堆元素的插入
- 判断堆数组满不满,不满则扩容
- 堆数组不满可以执行插入操作
- 向上调整(调成大/小根堆)
//堆的插入
public void offer(int val) {
//1.判断数组满不满,满则扩容
if(isFull()){
//扩容
this.elem = Arrays.copyOf(elem,2*elem.length);
}
//2. 容量不满,可以插入元素
elem[usedSize] = val; //插入数组的最后
usedSize++;
//3.开始向上调整
siftUp(usedSize-1); //根节点从0开始,上一步加加,找到最后一个结点下标必须要元素个数减1
}
private void siftUp(int child) {
int parent = (child - 1)/2; //根节点下标是0
while(child > 0) {
if(elem[child] > elem[parent]) {
//如果孩子结点比父亲结点大的话,就交换
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
//孩子结点要走到父亲节点位置,父亲结点也要向上走
child = parent;
parent = (child - 1)/2;
}else {
break;
}
}
}
public boolean isFull() {
return usedSize == elem.length; //等于数组长度就说明满了
}