优先级队列(PriorityQueue)
优先级队列
概念
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如 果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数 据结构就是优先级队列(Priority Queue)。
优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一 个一维数组中,并满足:Ki 且 Ki= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大 堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
堆的存储方式
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储
注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:
1.如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
2.如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
3.如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
堆的创建
堆向下调整
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?
仔细观察上图后发现:根节点的左右子树已经完全满足堆的性质,因只需将根节点向下调整好可。
向下过程(以小堆为例):
1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
2. 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在
1)parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
2)将parent与较小的孩子child比较,如果:
(1)parent小于较小的孩子child,调整结束
(2)否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。
堆的创建
那对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右子树不满足堆的特性,又该如何调整呢?
结论1:当我们采用向下调整去建堆的时候,时间复杂度为O(n);
代码示例:大根堆的【小根堆同样的道理】
package PriorityQueue;
import java.util.Arrays;
public class TestHeap {
private int[] elem; // 创建一个数组
private int usedSize; // 规定一个数组长度
// 再来一个构造方法
public TestHeap(){
this.elem = new int[10]; // 初始化一个数组,长度先暂且给一个10,不够再扩容
}
@Override
public String toString() {
return "TestHeap{" +
"elem=" + Arrays.toString(elem) +
'}';
}
// 初始化一个数组
public void initHeap(int[] array){
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++; // 放一个就加1
}
}
/**
* 创建一个大根堆
*/
public void createHeap(){
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]){
// 如果右孩子的值比左孩子大且没有越界
child++;
}
if (elem[child] > elem[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;
}
}
测试代码:
/**
* 测试大根堆的方法
* @param args
*/
public static void main(String[] args) {
TestHeap testHeap = new TestHeap();
int[] array = {27,15,19,18,28,34,65,49,25,37};
testHeap.initHeap(array);
testHeap.createHeap();
System.out.println(testHeap.toString());
System.out.println("=======================================================");
}
堆的插入与删除
堆的插入
堆的插入总共需要两个步骤:
1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
扩容的方法:
2. 将最后新插入的节点向上调整,直到满足堆的性质
代码示例:
/**
* 判断数组满没满,这是往数组里面插数据的必备前提
*/
public boolean idFull(){
return usedSize == elem.length; // 当数据个数等于属于长度,说明已经满了
}
/**
* 接下来往数组里面插元素
*/
public void offer(int val){
// 如果数组已经满了就需要扩容
if (idFull()){
this.elem = Arrays.copyOf(elem,2*elem.length);
}
this.elem[usedSize] = val;
// 向上调整
shiftUp(usedSize);
usedSize++;
}
/**
* 向上调整的代码
* @param
*/
private void shiftUp(int child) {
int parent = (child-1)/2;
while (child > 0){
// 当child大于0,说明还没调整完
if (elem[child] > elem[parent]){
swap(child,parent);
child = parent;
parent = (child-1)/2;
}else {
break;
}
}
}
堆的删除
注意:堆的删除一定删除的是堆顶元素。具体如下:
1. 将堆顶元素对堆中最后一个元素交换
2. 将堆中有效数据个数减少一个【usedSize--】
3. 对堆顶元素进行向下调整
4.注意:这里要分清楚什么是向上调整?什么是向下调整?
代码示例:
/**
* 向下调整代码
*/
public int poll(){
int tem = elem[0];
swap(0,usedSize-1);
usedSize--;
shiftDown(0,usedSize);
return tem;
}
private void shiftDown(int parent, int usedSize) {
// 重新定义一个孩子节点的下标
int child = (2*parent)+1;
while (child<usedSize){
// 进来这个循环的条件是“树还没调整完”
if (child+1<usedSize && elem[child] < elem[child+1]){
// 如果右孩子的值比左孩子大且没有越界
child++;
}
if (elem[child] > elem[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;
}
注意:小编这里的代码可能惠用到前面的一些方法,但是这一小编只把主要的代码给展示出来。
常用接口介绍
PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
关于PriorityQueue的使用要注意:
1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常.
那重写方法能不能改变不能比较得现状呢?
回答:
3. 不能插入null对象,否则会抛出NullPointerException(空指针异常)
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5. 插入和删除元素的时间复杂度为O(log2(N))
6. PriorityQueue底层使用了堆数据结构
7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
那能不能变成大根堆?
回答:可以的。如果需要大堆需要用户提供比较器。
PriorityQueue常用接口介绍
优先级队列的构造
此处只是列出了PriorityQueue中常见的几种构造方式
注:第三个调用的是一个比较器。【只要实现collection接口的,都可以用它来构造数据】
代码展示:
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue =
new PriorityQueue<>();
}
插入/删除/获取优先级最高的元素
代码展示:
/**
* PriorityQueue的构造
*/
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue =
new PriorityQueue<>();
// 往里面添加元素
priorityQueue.offer(10);
priorityQueue.offer(5);
priorityQueue.offer(9);
priorityQueue.offer(7);
priorityQueue.offer(77);
// 测试可见默认情况下是一个小根堆+删除元素
System.out.print(priorityQueue.poll()+" ");
System.out.print(priorityQueue.poll()+" ");
System.out.println();
System.out.println("=========");
// 获得有效数据
int size = priorityQueue.size();
System.out.println("堆的大小为:"+size);
System.out.println("==========");
// 判断队列是否为空 + 清空 + 再次判断队列是否为空
boolean result1 = priorityQueue.isEmpty();
System.out.println(result1);
priorityQueue.clear();
boolean result2 = priorityQueue.isEmpty();
System.out.println(result2);
}
扩容方式:
Top-k问题
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
核心思路:
1.当要找前K个最大值的时候,就建立一个K个节点的小堆
代码展示:方法+测试
/**
* 找前三个最大的元素:建小堆
*/
public int[] smalllestK(int[] array,int k){
int[] ret = new int[k]; // 建立一个数组来存放topk的元素
if (array == null || k <= 0){
return ret; // 直接返回一个ret;【就是什么都没有】
}
PriorityQueue<Integer> priorityQueue =
new PriorityQueue<>();
// 先用前k个元素建立一个小根堆
for (int i = 0; i < k; i++) {
priorityQueue.offer(array[i]);
}
// 再遍历数组中剩下的元素与小堆的堆顶元素比较
for (int i = k; i < array.length ; i++) {
int top = priorityQueue.peek(); // 先喵一眼,先不出
if (array[i] > top){
priorityQueue.poll();
priorityQueue.offer(array[i]);
}
}
for (int i = 0; i < k; i++) {
ret[i] = priorityQueue.poll();
}
return ret;
}
/**
* 测试找topK
*/
public static void main(String[] args) {
int[] array = {12,34,56,54,34231,23,56,89,21,11,3,4};
Test test = new Test();
int [] ret = test.smalllestK(array, 3);
System.out.println(Arrays.toString(ret));
}
代码展示2:当要求前K个小的元素,应该建立K个节点的大根堆
比较器:
class IntCmp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
方法代码:
/**
* 找后三个最小的元素:建大堆
*/
public int[] biglestK(int[] array,int k){
int[] ret = new int[k]; // 建立一个数组来存放topk的元素
if (array == null || k <= 0){
return ret; // 直接返回一个ret;【就是什么都没有】
}
PriorityQueue<Integer> priorityQueue =
new PriorityQueue<>(new IntCmp());
// 先用前k个元素建立一个小根堆
for (int i = 0; i < k; i++) {
priorityQueue.offer(array[i]);
}
// 再遍历数组中剩下的元素与小堆的堆顶元素比较
for (int i = k; i < array.length ; i++) {
int top = priorityQueue.peek(); // 先喵一眼,先不出
if (array[i] < top){
priorityQueue.poll();
priorityQueue.offer(array[i]);
}
}
for (int i = 0; i < k; i++) {
ret[i] = priorityQueue.poll();
}
return ret;
}
堆的应用
PriorityQueue的实现
用堆作为底层结构封装优先级队列
堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1.建堆
升序:建大堆
1)调整为大根堆
2)让第一个元素和最后一个未排序的元素进行交换
// 1.升序
public void heapSort(){
int end = usedSize-1;
while (end>0){
swap(0,end);
shiftDown(0,end);
end--;
}
}
在这里面的所有代码的方法在这个博客中都有涉及到,注意方法名字,就可以找到对应的代码了。
降序:建小堆
private void shiftDownMIN(int parent, int usedSize) {
// 重新定义一个孩子节点的下标
int child = (2*parent)+1;
while (child<usedSize){
// 进来这个循环的条件是“树还没调整完”
if (child+1<usedSize && elem[child] > elem[child+1]){
// 如果右孩子的值比左孩子大且没有越界
child++;
}
if (elem[child] < elem[parent]){
// 孩子节点的值比父母节点的大【左节点右节点都行】
swap(child,parent);
parent = child;
child = 2*parent+1;
}else {
break; // 本身是大根堆了
}
}
}
主代码:
// 2.降序
public void heapSortDesc() {
int end = usedSize - 1; // 最后一个元素的索引
while (end > 0) {
swap(0, end); // 将当前最小值放到数组末尾
shiftDownMIN(0, end); // 调整为小根堆
end--;
}
}
总的测试代码:
/**
* 测试堆排序的
*/
public static void main(String[] args) {
TestHeap testHeap = new TestHeap();
int[] array = {27,15,19,18,28,34,65,49,25,37};
testHeap.initHeap(array);
testHeap.createHeap();
System.out.println(testHeap.toString());
System.out.println("=======================================================");
testHeap.heapSort();
System.out.println(testHeap.toString());
System.out.println("=======================================================");
testHeap.heapSortDesc();
System.out.println(testHeap.toString());
System.out.println("=======================================================");
}
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
解析难点:
class IntCmp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
PriorityQueue<Integer> priorityQueue =
new PriorityQueue<>(new IntCmp());
为什么上述代码这么写就变成了大根堆呢?