前言
本blog将给大家优先级队列的实现及应用,我们知道数据结构中的队列遵循先进先出(FIFO)的原则,但是现实情况中任务通常都有优先级的概念,就得应用优先级队列的方式来解决,优先级队列的底层就是一个堆。
堆
基本概念
1)存储方式
堆通常的表示方式是,将完全二叉树用层序遍历的方式存储在数组中。
2)下标关系
在已知双亲(parent)的下标,则
left(左孩子下标) = 2 * parent + 1;
right(右孩子下标) = 2 * parent + 2;
在已知孩子下标(child),则
parent(父亲节点的下标) = (child - 1)/ 2;
1)堆逻辑上是一颗完全二叉树 2)堆在物理上是保存在数组中 3)大根堆:父亲节点的值大于子树中节点的值 4) 小根堆:父亲节点的值小于子树中节点的值 5)堆的基本作用是,快速找到集合的最值小根堆:
大根堆
向下调整算法
进行向下调整的前提是左右子树已经是一个大堆或则小堆。
1)向下调整的思想(以建大堆为例):
1.从根节点开始,选出左右孩子中的最大值;
2.将孩子的最大值与父亲节点的值进行比较;
3a.若大于父亲节点的值,则交换该子节点与父亲节点;
3b.若小于父亲节点的值,则这个堆就是大堆,不在需要进行调整了。
4.然后把交换后的子节点当成父亲节点,重复步骤2.3;
2)图示:
3)代码实现
public void ShiftDown2(int parent){
int child = 2 * parent + 1;
//当孩子节点的下标超过堆的长度调整结束
while (child < this.usedSize){
//得到孩子节点中的最大值
if (child + 1 < this.usedSize && this.elem[child] < this.elem[child + 1]){
child++;
}
//与父亲节点进行比较,若大于就进行交换
if (this.elem[child] > this.elem[parent]){
int tmp = this.elem[child];
this.elem[child] = this.elem[parent];
this.elem[parent] = tmp;
parent = child;
child = 2 * parent + 1;
}else {
break;
}
}
}
时间复杂度:最坏的情况是一直比较到叶子节点,比较的次数是完全二叉树的高度即为O(logn)。
建堆
把数组逻辑上可以看成一个完全二叉树,但是该数组还不是一个堆,现在我们通过向下调整算法把它构建成一个堆。我们从最后一个节点的父亲节点开始进行调整,一直调整到根节点,这个数组在逻辑上就变成了堆。
具体步骤:
1)通过最后一个节点的下标通过公式 parent = (child - 1) / 2
得到该父亲节点的下标。
2) 调整完成之后父亲节下标加一,一直调整到根节点,就构建了一个大堆。
代码实现:
public void createBigHeap1(int[] array){
int parent = (array.length - 1 - 1) / 2;
for (int i = parent; i >= 0 ; i--) {
shiftDown2(i);
}
}
public void shiftDown2(int parent){
int child = 2 * parent + 1;
//当孩子节点的下标超过堆的长度调整结束
while (child < this.usedSize){
//得到孩子节点中的最大值
if (child + 1 < this.usedSize && this.elem[child] < this.elem[child + 1]){
child++;
}
//与父亲节点进行比较,若大于就进行交换
if (this.elem[child] > this.elem[parent]){
int tmp = this.elem[child];
this.elem[child] = this.elem[parent];
this.elem[parent] = tmp;
parent = child;
child = 2 * parent + 1;
}else {
break;
}
}
}
时间复杂度分析:
建堆最坏的情况是,每一次都需要进行交换,所以总的时间复杂度为:总的时间复杂度为:
每一个需调整的节点的个数*每个节点需调整的次数
T(N) = 1*(h - 1) + 2 * (h - 2) + 2^2 * (h - 3) + … + 2^(h - 2) * 1;
两边乘以二
2 T(N) = 2*(h - 1) + 2^ 2* (h - 2) + 2^3 * (h - 3) + … + 2^(h - 1) * 1;
两式相减的
T(N) = 1 + 2 + 2^2 + … + 2 ^ (h - 2) + 2 ^(h - 1) - h;
T(N) = 2^h - h -1;
由二叉树的性质 有 N = 2^h -1 、 h= log(N + 1)
T(N) = N - log(N + 1)
当N 趋于无穷时
T(N) = O(N);
所以建堆的时间复杂度为:O(N)。
堆的应用 – 优先级队列
在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象吗,然后处理次高的对象。最简单的一个例子是,在手机玩游戏时,如果有来电,那么系统应该有限处理打进来的电话。在这种情况下,我们的数据结构应该提供两个基本的操作,一个是返回最高优先级对象,一个是添加新的对象,这种数据结构就是优先级队列(PriorityQueue).
入队列
1)(以大堆为例)入队列步骤(此过程也被称为向上调整):
1.首先在将新的对象按照尾插的方式放入数组。
2.比较其和双亲的值的大小,如果小于父亲节点的值,就满足堆的性质,插入结束
3.否则,与父亲节点交换值,重新进行2.3步骤
4.知道根节点结束。
2)图示
3)代码实现
public void shitfUp1(int child){
int parent = (child - 1) / 2;
while (parent >= 0){
if (this.elem[parent] < this.elem[child]){
int tmp = this.elem[parent];
this.elem[parent] = this.elem[child];
this.elem[child] = tmp;
child = parent;
parent = (child - 1) / 2;
}else {
break;
}
}
}
public void push1(int val){
if (isFull()){
this.elem = Arrays.copyOf(this.elem,2*this.usedSize);
}
this.elem[this.usedSize] = val;
this.usedSize++;
shitfUp1(this.usedSize-1);
}
出队列
为了防止破坏左右子树堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶的元素,然后通过向下调整方式重新调整为大堆。
代码实现
public void poll1(){
if (isEmpty()){
throw new RuntimeException("队列为空");
}
int tmp = this.elem[this.usedSize - 1];
this.elem[this.usedSize - 1] = this.elem[0];
this.elem[0] = tmp;
this.usedSize--;
shiftDown2(0);
}
PriorityQueue使用的注意事项
- 使用时需导入包;
- PriorityQueue中放置的元素必须要能够比较大小 (只有实现了 Comparable 和 Comparator 接口的类才能比较大小),不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常;
3.不能插入null对象,否则会空指针异常;
4.没有容量限制,内部会自动扩容;
5.插入和删除元素的时间复杂度均为O(logn);
6.底层是由堆实现。
优先级队列的应用 TOPK问题
解题思路:先用前k个元素生成一个大顶堆,这个大顶堆用于存储,当前最小的k个元素,接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素小于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。扫描完所有n-k个元素,最终堆中的k个元素,就是猥琐求的TopK。
题目:链接: 找出最小K个数.
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] ret = new int[k];
if(k == 0){
return ret;
}
//构建大堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>(){
@Override
public int compare(Integer o1,Integer o2){
return o2 - o1;
}
});
for(int i = 0; i < arr.length; i++){
//先放入k个元素如大堆中
if(maxHeap.size() < k){
maxHeap.offer(arr[i]);
}else{
//以后的元素与栈顶元素比较,小的元素与堆顶元素互换
int top = maxHeap.peek();
if(top > arr[i]){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
//将堆中元素放入数组中
for(int i = 0; i < k; i++){
ret[i] = maxHeap.poll();
}
return ret;
}
}
堆的应用-- 堆排序
堆排序在这篇文章中有详细的介绍:
链接: 七大经典排序.
创作不易,点个赞吧