1. 优先级队列
1.1 概念
队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
2. 优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
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 堆的存储方式
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,
从图上我们可以看出一般的二叉树来建立堆可能会导致空间浪费,对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树的性质对树进行还原。假设i为节点在数组中的下标,则有:
如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
2.3堆的创建
2.3.1 堆向下调整
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?
仔细观察上图后发现:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
public int[] elem;
//用来保存数组中元素的个数
public int usedSize;
//给数组默认初始值大小
public static final int DEFAULT_SIZE=10;
public Heap(){
this.elem=new int[DEFAULT_SIZE];
}
//将array数组存储到elem数组中
public void initElem(int[] array){
for (int i = 0; i < array.length; i++) {
elem[i]=array[i];
usedSize++;
}
}
向下过程(大根堆为例)
public void createHeap(){
//如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
//通过这个性质我们可以知道最后一棵子树的根节点为数组下标为4的节点
for (int parent = (usedSize-1-1)/2; parent >0 ; parent--) {
//向下调整,将根节点和数组长度传入
siftDown(parent,usedSize);
}
}
public void siftDown(int parent,int len){
// child先标记parent的左孩子,因为parent可能有左孩子左没有右孩子
int child = 2*parent+1;
while (child<len){
//如果child越界,就会报错,所以加入限制条件防止报错(child+1<len)
// 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
if (child+1<len&&elem[child]<elem[child+1]){
//就是将左右孩子的最大值的下标赋值给child
child=child+1;
}
if(elem[child]>elem[parent]){
//将双亲与较小的孩子交换
swap(child,parent);
// parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
大根堆为例:
2.3.3 建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是
近似值,多几个节点不影响最终结果):
因此:建堆的时间复杂度为O(n).
2.4 堆的插入与删除
2.4.1 堆的插入
堆的插入总共需要两个步骤:
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
小根堆为例添加元素:
public void push(int val){
//首先我们要判断元素是否已经存储满了,如果满了我们还会需要扩容
if(isFull()){
elem=Arrays.copyOf(elem,elem.length*2);
}
siftUp(usedSize);
usedSize++;
}
public void siftUp(int child){
int parent=(child-1)/2;
while (child>0){
if (elem[child]>elem[parent]){
swap(child,parent);
child=parent;
parent=(child-1)/2;
}else{
break;
}
}
}
采用向上调整添加元素
2.4.2 堆的删除
注意:堆的删除一定删除的是堆顶元素。具体如下:
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
public int pop(){
//判断是不是空
if(isEmpty()){
throw new Nornull("为空不能删除");
}
int oldVal=elem[0];
swap(0,usedSize);
usedSize--;
siftDown(0,usedSize);
return oldVal;
}
public void siftDown(int parent,int len){
//至少有左孩子
int child = 2*parent+1;
while (child<len){
//如果child越界,就会报错,所以
if (child+1<len&&elem[child]<elem[child+1]){
child=child+1;
}
if(elem[child]>elem[parent]){
swap(child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}