【数据结构】 优先级队列 ------ 堆
- 一、优先级队列
- 二、优先级队列的模拟实现
-
- [2.1 堆的概念](#2.1 堆的概念)
- [2.2 堆的性质](#2.2 堆的性质)
- [2.3 堆的存储方式(顺序存储 --> 存储到数组里)](#2.3 堆的存储方式(顺序存储 --> 存储到数组里))
- [2.4 堆的创建(以向下调整,创建大根堆为例)](#2.4 堆的创建(以向下调整,创建大根堆为例))
- [2.5 堆的插入与删除](#2.5 堆的插入与删除)
-
- [2.5.1 堆的插入(向上调整)](#2.5.1 堆的插入(向上调整))
- [2.5.2 堆的删除](#2.5.2 堆的删除)
- [2.6 用堆模拟实现优先级队列](#2.6 用堆模拟实现优先级队列)
- 三、PriorityQueue常用接口介绍
-
- [3.1 PriorityQueue的特性](#3.1 PriorityQueue的特性)
- [3.2 PriorityQueue常用接口](#3.2 PriorityQueue常用接口)
-
- [3.2.1 优先级队列的常用构造方法](#3.2.1 优先级队列的常用构造方法)
- [3.2.2 插入 / 删除 / 获取优先级最高的元素](#3.2.2 插入 / 删除 / 获取优先级最高的元素)
- 四、堆的应用
-
- [4.1 PriorityQueue的实现](#4.1 PriorityQueue的实现)
- [4.2 堆排序](#4.2 堆排序)
-
- [4.2.1 建堆](#4.2.1 建堆)
- [4.2.2 利用堆删除思想来进行排序](#4.2.2 利用堆删除思想来进行排序)
- [4.3 堆相关的 oj题](#4.3 堆相关的 oj题)
-
- [4.3.1 Top-k问题](#4.3.1 Top-k问题)
- [4.3.2 堆排序 ------ 对数据,从小到大排序](#4.3.2 堆排序 —— 对数据,从小到大排序)
- [4.4 堆相关的练习](#4.4 堆相关的练习)
一、优先级队列
1.概念
前⾯学习过队列,队列是⼀种先进先出(FIFO)的数据结构 ,但有些情况下,操作的数据可能带有优先级,⼀般出队列时,可能需要优先级高的元素先出队列,在下面的场景中,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象 。这种数据结构就是 优先级队列(Priority Queue)。
二、优先级队列的模拟实现
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 堆的性质
- 堆中某个节点的值总是不大于于或不小于其父节点的值;
- 堆总是⼀棵完全二叉树。
2.3 堆的存储方式(顺序存储 --> 存储到数组里)
从堆的概念可知,堆是一颗完全二叉树,因此可以层序的规则使用顺序的方式来高效存储 ,
注意:对于非完全二叉树,则不适合使用顺序方式进行存储 ,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据【数据结构】 二叉树 中 2.3二叉树的性质5对树进行还原。假设 i 为节点在数组中的下标,则有:

2.4 堆的创建(以向下调整,创建大根堆为例)
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成大根堆呢?
(1)手动调整 完全二叉树到大根堆 图解
(2)Java 代码实现 大根堆 思路
(3)Java 代码实现(向下调整)
TestHeap 类:
java
//向下调整 创建大根堆
public class TestHeap {
//存储数据
public int[] elem;
//usedSize: 存放有效数据的个数,假如数组长度为10,但只存了3个
public int usedSize;
public TestHeap(){
elem = new int[10];
}
public void init(int[] array){
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
//创建大根堆
public void creatHeap(){
for (int parent =(usedSize-1-1)/2;parent >= 0; parent--) {
//从每颗子树开始,向下调整
shiftDown(parent,usedSize);
}
}
private void shiftDown(int parent, int usedSize) {
int child = 2 * parent + 1;
//还没有调整完毕
while(child < usedSize){
if(child+1 < usedSize && elem[child] < elem[child+1]){
//创建小根堆,条件则为:
//if(child+1 < usedSize && elem[child] > elem[child+1])
child++;
}
//走到这里,child下标 一定是左右孩子最大值的下标
if(elem[child] > elem[parent]){
//创建小根堆,条件则为:
//if(elem[child] < elem[parent])
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
parent = child;
child = 2 * parent + 1;
}else{
break;
}
}
}
}
Test 类:
java
public class Test {
public static void main(String[] args) {
int[] array = {27,15,19,18,28,34,65,49,25,37};
TestHeap testHeap = new TestHeap();
testHeap.init(array);
testHeap.creatHeap();
System.out.println("========");
}
}
输出结果:

(4)堆向下调整 的时间复杂度:O(n)

2.5 堆的插入与删除
2.5.1 堆的插入(向上调整)
(1)图解
(2)Java 代码实现
TestHeap 类:
java
private void swap(int parent, int child) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
}
//向上调整 堆的插入
/**
* 插入数据:
* 每次插入到当前堆的最后一个(数组的最后一个),然后向上调整
* @param val
*/
public void offer(int val){
if(isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
//调整
shiftUp(usedSize);
usedSize++;
}
private void shiftUp(int child){
int parent = (child-1)/2;
while(parent >= 0){
if(elem[child] > elem[parent]){
swap(child,parent);
child = parent;
parent = (child-1)/2;
}else{
break;
}
}
}
public boolean isFull(){
return elem.length == usedSize;
}
输出结果:

2.5.2 堆的删除
(1)图解
(2)Java 代码实现
java
private void swap(int i, int j) {
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
//向上调整 堆的插入
/**
* 插入数据:
* 每次插入到当前堆的最后一个(数组的最后一个),然后向上调整
* @param val
*/
public void offer(int val){
if(isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
//调整
shiftUp(usedSize);
usedSize++;
}
private void shiftUp(int child){
int parent = (child-1)/2;
while(parent >= 0){
if(elem[child] > elem[parent]){
swap(child,parent);
child = parent;
parent = (child-1)/2;
}else{
break;
}
}
}
public boolean isFull(){
return elem.length == usedSize;
}
//删除堆
public int poll(){
if(isEmpty()){
//自己抛出异常
return -1;
}
int oldValue = elem[0];
swap(0,usedSize-1);
//调整0下标的树
usedSize--;
shiftDown(0,usedSize);
return oldValue;
}
public boolean isEmpty(){
return usedSize == 0;
}
Test 类:
java
testHeap.poll();
System.out.println("========");
输出结果:

2.6 用堆模拟实现优先级队列
java
public class MyPriorityQueue {
// 演⽰作⽤,不再考虑扩容部分的代码
private int[] array = new int[100];
private int size = 0;
public void offer(int e) {
array[size++] = e;
shiftUp(size - 1);
}
public int poll() {
int oldValue = array[0];
array[0] = array[--size];
shiftDown(0);
return oldValue;
}
public int peek() {
return array[0];
}
}
三、PriorityQueue常用接口介绍
3.1 PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列 ,
PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本⽂主要介绍 PriorityQueue。

java
import java.util.LinkedList;
import java.util.PriorityQueue;
class Student{
}
public class Test {
public static void main(String[] args) {
PriorityQueue<Student> queue = new PriorityQueue<>();
/*
//1. PriorityQueue中放置的元素必须要能够⽐较⼤⼩,
// 不能插⼊⽆法⽐较⼤⼩的对象,否则会抛出ClassCastException异常
queue.offer(new Student());//error
*/
/*//2.不能插⼊null对象,否则会抛出NullPointerException
queue.offer(null);//error*/
//LinkedList 可以存null
LinkedList<Student> queue1 = new LinkedList<>();
queue1.offer(null);
queue1.offer(null);
System.out.println(queue1.size());//2
}
}

3.2 PriorityQueue常用接口
3.2.1 优先级队列的常用构造方法

java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.PriorityQueue;
class Student{
}
public class Test {
public static void main(String[] args) {
//PriorityQueue(): 创建⼀个空的优先级队列,底层默认容量是11
PriorityQueue<Student> queue = new PriorityQueue<>();
//PriorityQueue(int intitialCapacity)
//创建⼀个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Student> queue2 = new PriorityQueue<>(15);
// ⽤ArrayList对象来构造⼀个优先级队列的对象
// queue3中已经包含了三个元素
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(10);
list.add(5);
PriorityQueue<Integer> queue3 = new PriorityQueue<>(list);
System.out.println("=======");
}
}
注意:默认情况下,PriorityQueue队列是⼩堆,如果需要⼤堆需要⽤⼾提供⽐较器。
下面的代码创建出来的就是⼀个大堆。
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.PriorityQueue;
// ⽤⼾⾃⼰定义的⽐较器:直接实现Comparator接⼝,然后重写该接⼝中的compare⽅法即可
class ImpLess implements Comparator<Integer>{
public int compare(Integer o1,Integer o2){
return o1.compareTo(o2);//小根堆
}
}
class ImpBig implements Comparator<Integer>{
public int compare(Integer o1,Integer o2){
return o2.compareTo(o1);//大根堆
}
}
public class Test {
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>(new ImpBig());
queue.offer(4);
queue.offer(3);
queue.offer(2);
queue.offer(1);
queue.offer(5);
System.out.println(queue.peek());
}
3.2.2 插入 / 删除 / 获取优先级最高的元素

代码示例:
四、堆的应用
4.1 PriorityQueue的实现
⽤堆作为底层结构封装优先级队列。
4.2 堆排序
堆排序即利⽤堆的思想来进⾏排序,总共分为两个步骤: (1)建堆。 (2)利用堆删除思想来进行排序。
4.2.1 建堆
- 升序:创建大堆
- 降序:创建小堆
4.2.2 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
具体详见本篇的 2.4 堆的创建 和 2.5 堆的插入与删除。
4.3 堆相关的 oj题
4.3.1 Top-k问题
TOP-K问题:即求数据集合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。

(2)图解

(3)Java 代码实现
解法1:
java
public class Test {
/**
* 解法1:不算是Topk 真正的解法
* 时间复杂度太高
* @param array
* @param k
* @return
*/
public static int[] topk1(int[] array,int k){
PriorityQueue<Integer> queue = new PriorityQueue<>();
//O(n*log2N)
for (int i = 0; i < array.length; i++) {
queue.offer(array[i]);
}
//O(k*logN)
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
ret[i] = queue.poll();
}
return ret;
}
public static void main(String[] args) {
int[] array = {1,12,3,41,5,16,7,81,9,10};
int[] ret = topk1(array,3);
System.out.println(Arrays.toString(ret));//[1, 3, 5]
}
解法3:
java
//TopK问题 解法3
class ImpBig implements Comparator<Integer>{
public int compare(Integer o1,Integer o2){
return o2.compareTo(o1);//大根堆
}
}
class Solution {
public static int[] smallestK(int[] array,int k){
int[] ret = new int[k];
if(k <=0 ){
return ret;
}
PriorityQueue<Integer> queue = new PriorityQueue<>(new ImpBig());
//O(k*logN)+O(N-K)*logK = O(N*logK)
for (int i = 0; i < k; i++) {
queue.offer(array[i]);
}
//从第K+1个元素,开始遍历: O(N-K)*logK
for (int i = k; i < array.length; i++) {
//先获取堆顶元素
int val = queue.peek();
if(val > array[i]){
queue.poll();
queue.offer(array[i]);
}
}
//走到这里,队列里面就是前K个最小的元素
for (int i = 0; i < k; i++) {
ret[i] = queue.poll();
}
return ret;
}
}
【TopK问题总结】
- 找前K个最小的数据 / 找到第K小的的元素 ------ 建立大根堆
- 找前K个最大的数据 / 找到第K大的的元素------ 建小根堆
4.3.2 堆排序 ------ 对数据,从小到大排序
对于集合{ 27,15,19,18,28,34,65,49,25,37 } 中的数据,如果将其从小到大排序呢?
(1)图解
(2)Java 代码实现
java
//对数据,从小到大排序
//时间复杂度:堆排序 O(N+log2N)
//空间复杂度:O(1)
public void heapSort(){
int end = usedSize - 1;
while(end > 0){
swap(0,end);
shiftDown(0,end);
end--;
}
}
输出结果:

4.4 堆相关的练习
