PriorityQueue优先级队列

偏执的太偏执、 2024-04-03 11:19 227阅读 0赞

文章目录

  • 1.概念
  • 2.优先级队列的模拟实现
    • 2.1 堆的概念
    • 2.2堆的创建
      • 2.2.1堆的向下调整
      • 2.2.2堆的构建
      • 2.2.3堆的插入
      • 2.2.4堆的删除
    • 2.3用堆模拟实现优先级队列
  • 3.常用接口介绍
    • 3.1PriorityQueue集合类源码分析
  • 4.堆的应用
    • 4.1 top-k问题最小的k个数
      • 思路1
      • 思路2
    • 4.2堆排序

1.概念

前面学过队列,队列是一种先进先出的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列。

这种数据结构提供两个最基本的操作,返回最高优先级对象和添加新的对象(出队和入队)。

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…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。PriorityQueue默认是小根堆。

对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节
点,就会导致空间利用率比较低。

2.2堆的创建

2.2.1堆的向下调整

对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆?

在这里插入图片描述

以大跟堆为例

思路:要想把这棵树变成大根堆,只需要将这棵树的每一棵子树都变成大根堆即可。

在这里插入图片描述
每一个绿框里都是一棵树,我们要把这些树都变成大根堆

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. private void shiftDown(int root,int len) {
  2. int parent=root;
  3. int child=parent*2+1;
  4. while(child<len){
  5. if(child+1<len&&elem[child]<elem[child+1]){
  6. //child+1可能会越界
  7. child++;
  8. }
  9. if(elem[child]>elem[parent]){
  10. int tmp=elem[child];
  11. elem[child]=elem[parent];
  12. elem[parent]=tmp;
  13. parent=child;
  14. child=parent*2+1;
  15. }else{
  16. //如果根节点大于孩子结点就不用向下遍历比较,因为就是从最后一棵子树向上调整的
  17. break;
  18. }
  19. }
  20. }

向下调整的时间复杂度:O(log2(n))
n个结点树的高度是log2(n),就是向下调整的次数

在这里插入图片描述

数组下标从后往前遍历,然后每个下标向下调整

2.2.2堆的构建

  1. public class PriorityQueue {
  2. public int[] elem;
  3. public int usedSize;
  4. public static final int DEFAULT_CAPACITY=10;
  5. public PriorityQueue() {
  6. elem=new int[DEFAULT_CAPACITY];
  7. }
  8. /**
  9. * 建堆的时间复杂度:O(n)
  10. *
  11. * @param array
  12. */
  13. public void createHeap(int[] array) {
  14. for(int i=0;i<array.length;i++){
  15. elem[i]=array[i];
  16. usedSize++;
  17. }
  18. for(int parent=(elem.length-1)/2;parent>=0;parent--){
  19. shiftDown(parent,elem.length);
  20. }
  21. }
  22. }
  23. public class Test {
  24. public static void main(String[] args) {
  25. int[] array={
  26. 27,15,19,18,28,34,65,49,25,37};
  27. PriorityQueue priorityQueue=new PriorityQueue();
  28. priorityQueue.createHeap(array);
  29. }
  30. }

建堆的时间复杂度:O(n)

在这里插入图片描述

2.2.3堆的插入

在这里插入图片描述

在数组最后位置插入一个数,(数组满了则要扩容)只需要向上调整,直到child=0
其他子树都是大根堆不需要调整

  1. public void push(int val) {
  2. if(isFull()){
  3. //扩容
  4. elem= Arrays.copyOf(this.elem,this.elem.length*2);
  5. }
  6. elem[usedSize]=val;
  7. usedSize++;
  8. shiftUp(usedSize-1);
  9. }
  10. private void shiftUp(int child) {
  11. int parent=(child-1)/2;
  12. while(child>0){
  13. if(elem[parent]<elem[child]){
  14. int tmp=elem[parent];
  15. elem[parent]=elem[child];
  16. elem[child]=tmp;
  17. child=parent;
  18. parent=(child-1)/2;
  19. }else{
  20. break;
  21. }
  22. }
  23. }
  24. public boolean isFull() {
  25. return elem.length==usedSize;
  26. }

插入后的结果:

在这里插入图片描述

2.2.4堆的删除

删除堆顶元素
将堆顶元素和最后一个元素交换,从堆顶开始向下调整

  1. /**
  2. * 出队【删除】:每次删除的都是优先级高的元素
  3. * 仍然要保持是大根堆
  4. */
  5. public void pollHeap() {
  6. if(isEmpty()){
  7. return;
  8. }
  9. int tmp=elem[0];
  10. elem[0]=elem[elem.length-1];
  11. elem[elem.length-1]=tmp;
  12. usedSize--;
  13. shiftDown(0,usedSize);
  14. }
  15. public boolean isEmpty() {
  16. return usedSize==0;
  17. }

结果:
在这里插入图片描述

2.3用堆模拟实现优先级队列

  1. import java.util.Arrays;
  2. public class PriorityQueue {
  3. public int[] elem;
  4. public int usedSize;
  5. public static final int DEFAULT_CAPACITY=10;
  6. public PriorityQueue() {
  7. elem=new int[DEFAULT_CAPACITY];
  8. }
  9. /**
  10. * 建堆的时间复杂度:O(n)
  11. *
  12. * @param array
  13. */
  14. public void createHeap(int[] array) {
  15. for(int i=0;i<array.length;i++){
  16. elem[i]=array[i];
  17. usedSize++;
  18. }
  19. for(int parent=(elem.length-1)/2;parent>=0;parent--){
  20. shiftDown(parent,elem.length);
  21. }
  22. }
  23. /**
  24. *
  25. * @param root 是每棵子树的根节点的下标
  26. * @param len 是每棵子树调整结束的结束条件
  27. * 向下调整的时间复杂度:O(logn)
  28. */
  29. private void shiftDown(int root,int len) {
  30. int parent=root;
  31. int child=parent*2+1;
  32. while(child<len){
  33. if(child+1<len&&elem[child]<elem[child+1]){
  34. child++;
  35. }
  36. if(elem[child]>elem[parent]){
  37. int tmp=elem[child];
  38. elem[child]=elem[parent];
  39. elem[parent]=tmp;
  40. parent=child;
  41. child=parent*2+1;
  42. }else{
  43. break;
  44. }
  45. }
  46. }
  47. /**
  48. * 入队:仍然要保持是大根堆
  49. * @param val
  50. */
  51. public void push(int val) {
  52. if(isFull()){
  53. //扩容
  54. elem= Arrays.copyOf(this.elem,this.elem.length*2);
  55. }
  56. elem[usedSize]=val;
  57. usedSize++;
  58. shiftUp(usedSize-1);
  59. }
  60. private void shiftUp(int child) {
  61. int parent=(child-1)/2;
  62. while(child>0){
  63. if(elem[parent]<elem[child]){
  64. int tmp=elem[parent];
  65. elem[parent]=elem[child];
  66. elem[child]=tmp;
  67. child=parent;
  68. parent=(child-1)/2;
  69. }else{
  70. break;
  71. }
  72. }
  73. }
  74. public boolean isFull() {
  75. return elem.length==usedSize;
  76. }
  77. /**
  78. * 出队【删除】:每次删除的都是优先级高的元素
  79. * 仍然要保持是大根堆
  80. */
  81. public void pollHeap() {
  82. if(isEmpty()){
  83. return;
  84. }
  85. int tmp=elem[0];
  86. elem[0]=elem[elem.length-1];
  87. elem[elem.length-1]=tmp;
  88. usedSize--;
  89. shiftDown(0,usedSize);
  90. }
  91. public boolean isEmpty() {
  92. return usedSize==0;
  93. }
  94. /**
  95. * 获取堆顶元素
  96. * @return
  97. */
  98. public int peekHeap() {
  99. if(isEmpty()){
  100. return -1;
  101. }
  102. return elem[0];
  103. }
  104. }

3.常用接口介绍

在这里插入图片描述

PriorityQueue默认是小根堆。

PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常

在这里插入图片描述

Student这个引用类型如果要进行比较,要实现Comparable<>接口,重写compareTo()方法

在这里插入图片描述

那么问题是它是什么时候在什么地方调用了我们重写的compareTo()方法?

3.1PriorityQueue集合类源码分析

分析PriorityQueue()构造方法:

  1. PriorityQueue<Student> priorityQueue=new PriorityQueue<>();

在这里插入图片描述

对offer()分析:

在这里插入图片描述
在这里插入图片描述

如果想实现大根堆,只需要修改compareTo()方法

  1. @Override
  2. public int compareTo(Student o) {
  3. return o.age-this.age;
  4. }

那如果我想要Integer类型的元素变成大根堆?
Integer源码不能修改compareTo()方法
所以就用到comparator比较器

  1. class IntCmp implements Comparator<Integer> {
  2. @Override
  3. public int compare(Integer o1, Integer o2) {
  4. return o2.compareTo(o1);
  5. }
  6. }
  7. public class Test {
  8. public static void main(String[] args) {
  9. PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(new IntCmp());
  10. priorityQueue.offer(10);
  11. priorityQueue.offer(20);
  12. priorityQueue.offer(3);
  13. System.out.println(priorityQueue.poll());
  14. System.out.println(priorityQueue.poll());
  15. System.out.println(priorityQueue.poll());
  16. }
  17. }

在这里插入图片描述

当没有传入数组容量时,默认容量是11,
当没有比较器时,必须是可比较的
优先使用比较器

  1. //匿名内部类
  2. public static void main(String[] args) {
  3. PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(new Comparator<Integer>() {
  4. @Override
  5. public int compare(Integer o1, Integer o2) {
  6. return o2.compareTo(o1);
  7. }
  8. });
  9. }
  10. //lambda表达式
  11. PriorityQueue<Integer> priorityQueue2=new PriorityQueue<>((x,y)->{
  12. return x.compareTo(y);});
  13. PriorityQueue<Integer> priorityQueue3=new PriorityQueue<>((x,y)->x.compareTo(y));

在这里插入图片描述
PriorityQueue是如何扩容的?
在这里插入图片描述

如果增加一个元素数组满了就扩容,根据原来数组的容量和64比较来决定是2倍扩还是1.5倍扩。扩容后新的容量如果大于整数最大值-8要进行调整,minCapacity是原来数组容量+1,如果minCapacity>整数最大值-8,就调整为整数最大值,否则就给整数最大值-8。

4.堆的应用

4.1 top-k问题最小的k个数

在这里插入图片描述
能想到的最简单直接的方式就是排序,但是如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决。

思路1

把arr数组所有元素建成小根堆,出队前k个元素,每出队1个,自动调整成小根堆,每次弹出的都是堆顶元素,自然是最小的。

时间复杂度:O(n+klogn)
建堆:O(n)
调整:O(klogn)

  1. public static int[] smallestK2(int[] arr, int k) {
  2. //1.建立一个小根堆
  3. PriorityQueue<Integer> minHeap=new PriorityQueue<>();
  4. //2.遍历数组,将数组的每个元素放到小根堆里
  5. for (int i = 0; i < arr.length; i++) {
  6. minHeap.offer(arr[i]);
  7. }
  8. //3.弹出k个元素,放到数组中,返回即可
  9. int[] tmp=new int[k];
  10. for (int i = 0; i < k; i++) {
  11. tmp[i]=minHeap.poll();
  12. }
  13. return tmp;
  14. }

如果想降低时间复杂度,下面给出第二种做法。

思路2

将前k个元素建成大根堆,遍历数组中剩下的元素,如果比堆顶元素小,则弹出堆顶元素,然后offer(),并调整为大根堆,遍历结束后堆里就是最小的k个数。
(和思路一的区别是没有整体建堆)

这里的k是很小的

时间复杂度:O(nlogk)
O(k+(n-k)logk)=O(k+nlogk-klogk)=O(nlogk)

  1. class Solution {
  2. public int[] smallestK(int[] arr, int k) {
  3. PriorityQueue<Integer> maxHeap=new PriorityQueue<>(k, new Comparator<Integer>() {
  4. @Override
  5. public int compare(Integer o1, Integer o2) {
  6. return o2.compareTo(o1);
  7. }
  8. });
  9. for(int i=0;i<arr.length;i++){
  10. if(maxHeap.size()<k){
  11. maxHeap.offer(arr[i]);
  12. }else{
  13. int val=maxHeap.peek();
  14. if(arr[i]<val){
  15. maxHeap.poll();
  16. maxHeap.offer(arr[i]);
  17. }
  18. }
  19. }
  20. int[] tmp=new int[k];
  21. for (int i = 0; i < k; i++) {
  22. tmp[i]=maxHeap.poll();
  23. }
  24. return tmp;
  25. }
  26. }

在这里插入图片描述
这样写抛异常了,原因是没有判断k是否合法
在这里插入图片描述
正确写法:

  1. class Solution {
  2. public int[] smallestK(int[] arr, int k) {
  3. if(arr==null||k==0){
  4. return new int[0];
  5. }
  6. PriorityQueue<Integer> maxHeap=new PriorityQueue<>(k, new Comparator<Integer>() {
  7. @Override
  8. public int compare(Integer o1, Integer o2) {
  9. return o2.compareTo(o1);
  10. }
  11. });
  12. for(int i=0;i<arr.length;i++){
  13. if(maxHeap.size()<k){
  14. maxHeap.offer(arr[i]);
  15. }else{
  16. int val=maxHeap.peek();
  17. if(arr[i]<val){
  18. maxHeap.poll();
  19. maxHeap.offer(arr[i]);
  20. }
  21. }
  22. }
  23. int[] tmp=new int[k];
  24. for (int i = 0; i < k; i++) {
  25. tmp[i]=maxHeap.poll();
  26. }
  27. return tmp;
  28. }
  29. }

如果要求第k小的,就是大根堆堆顶元素

4.2堆排序

利用堆的思想来排序

升序:建大根堆

要想实现升序,如果建小根堆,然后分层弹出,还有另外设一个空间来存放这些弹出的数据,空间复杂度是O(n)。况且小根堆中的元素也不一定是升序的。

如果考虑建大根堆,让大根堆堆顶元素和最后一个交换,然后再向下调整成大根堆,然后再让大根堆堆顶元素和最后一个交换,并向下调整成大根堆…以此类推。

在这里插入图片描述

  1. public class TestHeap {
  2. public int[] elem;
  3. public int usedSize;
  4. public static final int DEFAULT_CAPACITY=10;
  5. public TestHeap() {
  6. elem=new int[DEFAULT_CAPACITY];
  7. }
  8. /**
  9. * 建堆的时间复杂度:O(n)
  10. *
  11. * @param array
  12. */
  13. public void createHeap(int[] array) {
  14. for(int i=0;i<array.length;i++){
  15. elem[i]=array[i];
  16. usedSize++;
  17. }
  18. for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
  19. shiftDown(parent,usedSize);
  20. }
  21. }
  22. }
  23. private void shiftDown(int root,int len) {
  24. int parent=root;
  25. int child=parent*2+1;
  26. while(child<len){
  27. if(child+1<len&&elem[child]<elem[child+1]){
  28. child++;
  29. }
  30. if(elem[child]>elem[parent]){
  31. int tmp=elem[child];
  32. elem[child]=elem[parent];
  33. elem[parent]=tmp;
  34. parent=child;
  35. child=parent*2+1;
  36. }else{
  37. break;
  38. }
  39. }
  40. }
  41. public void heapSort(){
  42. int end=usedSize-1;
  43. while(end>0){
  44. int tmp=elem[0];
  45. elem[0]=elem[end];
  46. elem[end]=tmp;
  47. shiftDown(0,end);
  48. end--;
  49. }
  50. }
  51. public static void main5(String[] args) {
  52. int[] array={
  53. 27,15,19,18,28,34,65,49,25,37};
  54. TestHeap testHeap=new TestHeap();
  55. testHeap.createHeap(array);
  56. testHeap.heapSort();
  57. }

时间复杂度:O(n)+O(nlogn)约等于O(nlogn)
空间复杂度:O(1)

发表评论

表情:
评论列表 (有 0 条评论,227人围观)

还没有评论,来说两句吧...

相关阅读

    相关 PriorityQueue(优先级队列总结)

    一,概念 > 队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列 > 数据结构应该