当前位置:   article > 正文

数据结构——优先级队列(堆)Priority Queue详解

数据结构——优先级队列(堆)Priority Queue详解

1. 优先级队列

队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该场景下,使用队列不合适

在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象,这种数据结构就是优先级队列(Priority Queue)

2. 优先级队列的模拟实现

2.1 堆的概念

性质:

如上图,堆中某个节点的值总是不大于其父节点的值,称为最小堆/小根堆

堆中某个节点的值总是不小于其父节点的值,称为最大堆/大根堆

堆总是一棵完全二叉树

2.2 堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可用层序的规则采用顺序的方式进行高效存储

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

将元素存储到数组中后,假设 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 }为例,转换为大根堆的过程:

  1. //TestHeap 类
  2. public class TestHeap {
  3. public int[] elem;//堆由数组实现
  4. public int usedSize;//记录有效数据个数
  5. public TestHeap() {//构造方法
  6. this.elem = new int[10];
  7. }
  8. public void init(int[] array) {//给数组初始化
  9. for (int i = 0; i < array.length; i++) {
  10. elem[i] = array[i];
  11. usedSize++;
  12. }
  13. }
  14. //把elem数组中的数据调整为大根堆
  15. public void createHeap() {
  16. //让parent初始位置在树的最后一棵子树的根节点处,用到公式:根节点 = (i-1)/2
  17. //其中usedSize-1表示最后一个节点,从上到下循环遍历整棵树
  18. for (int parent = (usedSize-1-1)/2; parent >= 0; parent--) {
  19. //siftDown方法用来判断树中左右孩子与父亲节点的大小关系
  20. siftDown(parent,usedSize);
  21. }
  22. }
  23. //交换函数
  24. public void swap(int i, int j) {
  25. int tmp = elem[i];
  26. elem[i] = elem[j];
  27. elem[j] = tmp;
  28. }
  29. public void siftDown(int parent, int end) {
  30. int child = 2*parent+1;//公式:左孩子下标 = 2*根节点下标+1
  31. while(child < end) {//end是有效数据个数,不能用<=
  32. //下面if走完之后,child一定是左右孩子中最大值的下标
  33. if(child+1 < end && elem[child] < elem[child+1]) {
  34. child++;
  35. }
  36. //下面if判断孩子节点是否大于父亲节点,若大于则交换,否则跳出循环,去判断上一棵树
  37. if(elem[child] > parent) {
  38. swap(child,parent);
  39. parent = child;
  40. child = 2*parent+1;
  41. }else {
  42. break;
  43. }
  44. }
  45. }
  46. }
  47. //Test 类
  48. public class Test {
  49. public static void main(String[] args) {
  50. int[] array = {27,15,19,18,28,34,65,49,25,37};
  51. TestHeap testHeap = new TestHeap();
  52. testHeap.init(array);
  53. testHeap.createHeap();
  54. }
  55. }

tip:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆,才可以向下调整

时间复杂度:最坏的情况就如上例,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(\log _{2}N)

2.3.2 建堆的时间复杂度

推导:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了方便计算,使用满二叉树来推导

结论: 建堆的时间复杂度为 O(N)

2.4 堆的插入与删除

2.4.1 堆的插入

堆的插入需要两步

1. 先将元素放到树底层最后一个节点(空间不够时扩容)

2. 将插入新节点向上调整,直到满足堆的性质

  1. public void offer(int val) {
  2. if(isFull()) {//判满,若满则扩容
  3. elem = Arrays.copyOf(elem,2*elem.length);
  4. }
  5. elem[usedSize] = val;//val赋值到usedSize位置,usedSize++
  6. usedSize++;
  7. siftUp(usedSize-1);//向上调整
  8. }
  9. public void siftUp(int child) {
  10. int parent = (child-1)/2;//通过孩子节点找到父亲节点
  11. while(parent >= 0) {//当父亲节点下标小于0,说明向上调整结束了,堆已有序
  12. if(elem[child] > elem[parent]) {//孩子节点的值大于父亲节点值,即交换
  13. swap(child,parent);
  14. child = parent;
  15. parent = (child-1)/2;
  16. }else {//若孩子节点的值小于父亲节点的值,说明堆已有序,直接跳出循环
  17. break;
  18. }
  19. }
  20. }
  21. public boolean isFull() {
  22. return usedSize == elem.length;
  23. }

2.4.2 堆的删除

tip:堆的删除一定删除的是堆顶元素,分为三步:

1. 将堆定元素对堆中最后一个元素交换

2. 将堆中有效数据个数减少一个

3. 对堆顶元素进行向下调整

  1. public int poll() {
  2. if(isEmpty()) {//若栈空,返回-1
  3. return -1;
  4. }
  5. int old = elem[0];//记录要删除的数据,最后返回
  6. swap(0,usedSize-1);//交换堆顶元素和最后元素
  7. usedSize--;//有效个数--
  8. siftDown(0,usedSize);//从堆定开始,向下调整
  9. return old;
  10. }
  11. public boolean isEmpty() {
  12. return usedSize == 0;
  13. }

例题:

1.下列关键字序列为堆的是:()

A: 100,60,70,50,32,65   B: 60,70,65,50,32,100   C: 65,100,70,32,50,60

D: 70,65,100,32,50,60   E: 32,50,100,70,65,60   F: 50,100,70,65,60,32

选A

2.已知小根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的比较次数是()

A: 1         B: 2          C: 3        D: 4

解析:选C,15与10比,12与10比,12与16比

3.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()

A: [3,2,5,7,4,6,8]        B: [2,3,5,7,4,6,8]

C: [2,3,4,5,7,8,6]        D: [2,3,4,5,6,7,8]

选C

3. 常用接口介绍

3.1 PriorityQueue的特性

Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue

  1. public static void main(String[] args) {
  2. PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
  3. priorityQueue.offer(1);
  4. priorityQueue.offer(2);
  5. priorityQueue.offer(3);
  6. System.out.println(priorityQueue.poll());//运行结果:1
  7. System.out.println(priorityQueue.poll());// 2
  8. }

关于PriorityQueue的使用注意:

  • 1.使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
  • 2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常 
  • 3. 不能插入null对象,否则会抛出 NullPointerException
  • 4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  • 5. 插入和删除元素的时间复杂度为O(log_2N)
  • 6. PriorityQueue底层使用了堆数据结构
  • 7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素

3.2 PriorityQueue常用接口介绍

3.2.1 优先级队列的构造

此处只是常见的几种:

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异 常
PriorityQueue(Collection c)用一个集合来创建优先级队列

构造方法原理:

用一个集合来创建优先级队列:

  1. class Student {
  2. }
  3. public class Test {
  4. public static void main(String[] args) {
  5. ArrayList<Integer> arrayList = new ArrayList<>();
  6. arrayList.add(1);
  7. arrayList.add(2);
  8. arrayList.add(3);
  9. PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(arrayList);
  10. priorityQueue.offer(4);
  11. priorityQueue.offer(5);
  12. priorityQueue.offer(6);
  13. System.out.println(priorityQueue.poll());//运行结果:1
  14. System.out.println(priorityQueue.poll());// 2
  15. PriorityQueue<Student> priorityQueue1 = new PriorityQueue<>(arrayList);//error
  16. //传入参数必须是Student类或其子类
  17. }
  18. }

3.2.2 offer元素原理:

查看源码:

结合上面创建小根堆的流程分析:

自己实现比较器:

  1. //实现小根堆的比较器
  2. class IntCmp implements Comparator<Integer> {
  3. @Override
  4. public int compare(Integer o1, Integer o2) {
  5. return o1.compareTo(o2);//小根堆
  6. }
  7. }
  8. public class Test {
  9. public static void main(String[] args) {
  10. PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());//别忘了此处new比较器对象作为参数
  11. priorityQueue.offer(4);
  12. priorityQueue.offer(5);
  13. priorityQueue.offer(6);
  14. System.out.println(priorityQueue.poll());//运行结果:4
  15. System.out.println(priorityQueue.poll());// 5
  16. }
  17. }
  18. //实现大根堆的比较器
  19. class IntCmp implements Comparator<Integer> {
  20. @Override
  21. public int compare(Integer o1, Integer o2) {
  22. return o2.compareTo(o1);//大根堆 两比较器仅有此处不同!!!!!!!!!!!!!!
  23. }
  24. }
  25. public class Test {
  26. public static void main(String[] args) {
  27. PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());
  28. priorityQueue.offer(4);
  29. priorityQueue.offer(5);
  30. priorityQueue.offer(6);
  31. System.out.println(priorityQueue.poll());//运行结果:6
  32. System.out.println(priorityQueue.poll());// 5
  33. }

3.2.3 PriorityQueue的扩容方式

以下是JDK17中的源码:

说明:

如果容量小于64时,按照oldCapacity的2倍方式扩容

如果容量大于等于64,按照oldCapacity的1.5倍方式扩容

如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

3.3 OJ练习

top-k问题:最大或最小的前k个数据

  1. public static int[] smallestK(int[] arr, int k) {
  2. PriorityQueue<Integer> minHeap = new PriorityQueue<>();
  3. for (int i = 0; i < arr.length; i++) {
  4. minHeap.offer(arr[i]);
  5. }
  6. int[] tmp = new int[k];
  7. for (int i = 0; i < k; i++) {
  8. int val = minHeap.poll();
  9. tmp[i] = val;
  10. }
  11. return tmp;
  12. }
  13. public static void main(String[] args) {
  14. int[] array = {27,15,19,18,28};
  15. int[] ret = smallestK(array,3);
  16. System.out.println(Arrays.toString(ret));
  17. }

运行结果:

虽然上述方法也能满足需求,但是其时间复杂度为O((N+K)*\log _{2}N)

不是非常好的解决方案,现需要一个时间复杂度更小的方法:

若求前K个最小的数字

1. 先把前 个元素建立大小为K的大根堆

2. 遍历剩余 N-K 个元素,若堆顶元素大于当前 i 下标的值就出堆

这样遍历完数组后,大小为 K 的堆中就是最小的 K 个元素

反之求前K个最大的数字就建立小根堆

  1. class IntCmp implements Comparator<Integer> {
  2. @Override
  3. public int compare(Integer o1, Integer o2) {
  4. return o2.compareTo(o1);//大根堆
  5. }
  6. }
  7. class Solution {
  8. public int[] smallestK(int[] arr, int k) {
  9. int[] tmp = new int[k];
  10. if(k == 0) {
  11. return tmp;
  12. }
  13. PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new IntCmp());
  14. //1.把前K个元素放进堆中
  15. for (int i = 0; i < k; i++) {
  16. maxHeap.offer(arr[i]);
  17. }
  18. //2.遍历剩下的 N-k 个元素
  19. for (int i = k; i < arr.length; i++) {
  20. int val = maxHeap.peek();
  21. if(val > arr[i]) {
  22. maxHeap.poll();
  23. maxHeap.offer(arr[i]);
  24. }
  25. }
  26. //3.将堆中元素放到数组中
  27. for (int i = 0; i < k; i++) {
  28. tmp[i] = maxHeap.poll();
  29. }
  30. return tmp;
  31. }
  32. }

变种题:求第K小的数据,建立大根堆,堆顶元素就是第K小

3.4 堆排序

要求:在原堆上修改,不能新建

方法两步:

1. 建堆:

  • 要求升序,建大堆
  • 要求降序,建小堆

2. 利用堆删除来进行排序

建堆和堆删除中都用到了向下调整

  1. //堆排序 升序
  2. public void heapSort() {
  3. int endIndex = usedSize-1;
  4. while(endIndex > 0) {
  5. swap(0,endIndex);
  6. siftDown(0,endIndex);
  7. endIndex--;
  8. }
  9. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/900810
推荐阅读
相关标签
  

闽ICP备14008679号