• Java数据结构—优先级队列(堆)


    在学习完二叉树后,我们来学习优先级队列(堆)😎 

    🌱优先级队列(堆)定义

    所谓优先级队列,其实也就是一个按顺序存储方式来进行的完全二叉树,在一维数组中按照层序遍历的顺序存储。

    ✨堆的性质

    堆的节点总是大于或者小于其子结点,且堆总是一棵完全二叉树

    ✨堆的种类

    堆分为大根堆和小根堆。

    ⭕大根堆:大根堆的每个节点都大于其孩子节点。其图示如下👇

    ⭕小根堆:小根堆的每个节点都小于其孩子节点。其图示如下👇

     以此我们可以知道,如果不是完全二叉树,那么数据不适合用数组来呈现,也就是不适合成为堆。


    🌱优先级队列(堆)的仿写

    要了解一个数据结构,我们可以从仿写入手,显示创建一个堆(以大根堆为例)

    创建大根堆 

    首先我们有一个数组,里面存放着11,18,4,65,89,32,16这些数据。若要使得该数组变为大根堆,我们需要做到:从最后一个子树开始往前,直至最开始的树,每到一个树都向下调整

    🟢什么是向下调整?

    向下调整即本次根节点及其子节点中,若是创建大根堆,即找到三个节点中的最大值,放到根节点的位置上;若是创建小根堆,即找到三个节点中的最小值,放到根节点的位置上。图示如下👇

     在了解完什么是向下调整后,我们还有一些疑问:①要怎么样才可以找到最后一个子树;②在调整完当前子树后,怎么样走到下一棵子树?

    🟢问题①:

    我们要找到最后一颗子树,只需要运用之前在树中学习的知识,用(最后一个节点位置-1)/2   这个公式来求得最后一颗子树的根节点位置。

    🟢问题②:

    要走到下一个位置,我们知道这个是用数组的形式来存储的,使用只需要求得最后一颗子树位置后,该位置持续--就好。

    🔴特别注意:需要特别注意的是,我们的向下调整指的是在调整完当前子树后,其子树的子树也是大根堆的状态

    在解决完上面的疑问后,写出下面代码😎(具体操作见注释)

    1. public class TeatHeap {
    2. public int[] arr = {11,18,4,65,89,32,16}; //堆是以数组的方式存储的
    3. public void creatHeap(){
    4. //最后一棵子树的位置
    5. int root = (arr.length-1-1) / 2;
    6. while(root>=0){
    7. shiftDown(root); //一直向下调整直至调整到最后一颗树,也就是0位置上的整棵树
    8. root--;
    9. }
    10. }
    11. public void shiftDown(int root){
    12. int parent = root;
    13. int child = parent * 2 + 1; //孩子节点记得加一
    14. //这里使用while语句,是为了确保在当前子树创建完后其子树的子树也是大根堆
    15. while(child//向下调整,要一路顺下去,保证是正确的
    16. if(child+1arr[child+1])){ //这里是看除了左孩子节点外,还有没有右孩子节点,如果有的话要找出来大的那个,以便之后替换
    17. child = child+1;
    18. }
    19. //还要判断该子树的根节点是不是最大的,是就直接break,因为已经是大根堆了,不是再交换变成大根堆
    20. if(arr[parent]
    21. break;
    22. }else{
    23. int str = this.arr[parent];
    24. this.arr[parent] = this.arr[child];
    25. this.arr[child] = str;
    26. parent = child;
    27. child = parent * 2 + 1;
    28. }
    29. }
    30. }

     在完成大根堆的创造后,创建小根堆也很容易,只需要将判断大小的代码child改为childarr[child]即可✨

    🟢创建大根堆的时间复杂度

    大根堆的创建的时间复杂度是O(n),具体的时间复杂度推算如下👇:

     ✨堆的插入

    我们的堆在插入一个数据后要保证其还是大根堆或小根堆。

    因为我们插入的数据放在数组的最后一个位置上,所以这最后的数据一定是孩子节点,为了使得堆为大根堆,我们需要从孩子节点开始,向上调整,就是为了保证每个孩子节点对应的子树都是大根堆,其原理与向下调整相似。具体代码如下👇:

    1. public boolean isFull(){
    2. return arraySize>=arr.length;
    3. }
    4. private void shiftUp(int child){
    5. int parent = (child-1) / 2;
    6. while(child>0){
    7. if(arr[child]>arr[parent]){
    8. int str = arr[child];
    9. arr[child] = arr[parent];
    10. arr[parent] = str;
    11. child = parent;
    12. parent = (child-1)/2;
    13. }else {
    14. break;
    15. }
    16. }
    17. }
    18. public void push(int val){
    19. if(isFull()){ //判断是不是满的数组,是就要扩容
    20. arr = Arrays.copyOf(arr, arr.length*2);
    21. }
    22. int child = arraySize; //在数组尾插入数据,之后开始向上调整
    23. arr[child] = val;
    24. shiftUp(child);
    25. arraySize++;
    26. }

    ✨堆的删除

    堆的删除就是把堆的最上层元素与最后的元素对调,然后再重新从根开始,向下调整,便完成了删除操作。过程可参照下图👇:

    具体代码如下👇:

    1. public boolean isEmpty(){
    2. return arraySize<=0;
    3. }
    4. public void pop(){
    5. if(isEmpty()){
    6. System.out.println("该堆为空");
    7. return;
    8. }
    9. int str = arr[0];
    10. arr[0] = arr[arraySize-1];
    11. arr[arraySize-1] = str;
    12. arraySize--;
    13. shiftDown(0);
    14. }

    🌱优先级队列的应用—TopK问题、堆排序

    ✨TopK问题

    请问我们要获得一个数组里面的前k个最大值,要怎么实现?

    要解决上述问题,我们只需要将该数组变为一个大根堆,然后弹出一个后再变为大根堆,这样子弹出的便依次是第一个最大数、第二个最大数、第三个...

    在这之前我们先需要创建一个大根堆,那么要怎么样才能够创建一个大根堆或是小根堆呢?具体代码和注释如下👇

    1. import java.util.Comparator;
    2. import java.util.PriorityQueue;
    3. class IntCmp implements Comparator { //写一个比较器,若o1-o2则是小根堆,o2-o1则是大根堆
    4. @Override
    5. public int compare(Integer o1, Integer o2) {
    6. return o2 - o1;
    7. }
    8. }
    9. public class demo1 {
    10. public static void main(String[] args) {
    11. IntCmp intCmp = new IntCmp();
    12. PriorityQueue priorityQueue = new PriorityQueue<>(intCmp); //在这里放入比较器
    13. priorityQueue.offer(11);
    14. priorityQueue.offer(18);
    15. priorityQueue.offer(4);
    16. priorityQueue.offer(65);
    17. priorityQueue.offer(89);
    18. priorityQueue.offer(32);
    19. System.out.println("大怨种");
    20. }
    21. }

    我们可以先把IntCmp里面的o2-o1改为o1-o2,此时创建的就会是小根堆,在下图的调试中可以看到👇:

    然后把IntCmp里面的o1-o2改为o2-o1,此时创建的就会是小根堆,在下图的调试中可以看到👇:

     在我们解决了大根堆小根堆的创建后,就只需要将元素pop出即可😎

    但是其实这样的时间复杂度还是过于高了,原因是我们每弹出一个元素,就需要向下调整到最后。

    于是,我们便有另一种思路:因为要获得前k个最大元素,我们只需要先创建一个小根堆,然后先将k个元素塞进去,之后就只关注这k个元素组成的堆,再在放入元素的时候先对堆顶元素对比,如果比堆顶元素大,则堆顶元素弹出,将新的元素放入,然后再从堆顶元素开始,向下调整,这样子就可以大大减少时间复杂度了😎

    ❗❗注意,如果要前k个最大数则要创建小根堆;求前k个最小值则需要创建大根堆❗❗

    画图演示如下👇:

    在画完图后,大家可以尝试一下自己写出代码,以下的博主写出的代码😎

    1. import java.util.Comparator;
    2. import java.util.PriorityQueue;
    3. class IntCmp implements Comparator { //写一个比较器,若o1-o2则是小根堆,o2-o1则是大根堆
    4. @Override
    5. public int compare(Integer o1, Integer o2) {
    6. return o1 - o2;
    7. }
    8. }
    9. public class demo1 {
    10. public static IntCmp intCmp = new IntCmp();
    11. public static PriorityQueue priorityQueue = new PriorityQueue<>(intCmp); //在这里放入比较器
    12. public static void TopK(int[] arr,int k){
    13. for (int i = 0; i < k; i++) {
    14. priorityQueue.offer(arr[i]);
    15. }
    16. for (int i = k; i < arr.length; i++) {
    17. if(arr[i]>priorityQueue.peek()){
    18. priorityQueue.poll();
    19. priorityQueue.offer(arr[i]);
    20. }
    21. }
    22. for (int i = 0; i < k; i++) {
    23. System.out.println(priorityQueue.poll());
    24. }
    25. }
    26. public static void main(String[] args) {
    27. int[] arr = {11,18,4,32,89,65};
    28. TopK(arr,3);
    29. }
    30. }

    在完成上面的所有代码后,我们就可以写LeetCode上的题目——最小K个数 😎

    ✨堆排序

    堆排序就是将堆从大到小排序或者是从小到大排序。

    我们堆排序的操作如下:假设创建从小到大排序,那么我们就需要创建一个大根堆,元素个数为k,将这个大根堆先向下调整,保证堆顶元素是最大的,然后将堆顶元素与最后一个元素位置调换,然后从下标为0开始(即堆顶位置),向下调整至k-2的位置,如果有两个元素已经从堆顶调换位置,那么就向下调整到k-3的位置,以此类推...这样我们就能保证每次将堆顶元素放到该排序的位置上了。具体图示如下👇

    Tips:在这里我们将shiftDown函数稍作调整,变为两个形参,分别命名为int root和int end,以方便操作。并且要注意在调换完位置后,end--之前进行向下调整,否则会走到最后俩个元素的时候导致顺序不对

    具体代码和注释如下👇:

    1. import java.util.Arrays;
    2. import java.util.PriorityQueue;
    3. public class TeatHeap {
    4. public int[] arr = {11,18,4,32,89,65};
    5. public int arraySize = arr.length;
    6. public void creatHeap(){
    7. //最后一棵子树的位置
    8. int root = (arr.length-1-1) / 2;
    9. while(root>=0){
    10. shiftDown(root,arr.length); //一直向下调整直至调整到最后一颗树,也就是0位置上的整棵树
    11. root--;
    12. }
    13. }
    14. public void shiftDown(int root,int end){
    15. int parent = root;
    16. int child = parent * 2 + 1; //孩子节点记得加一
    17. //这里使用while语句,是为了确保在当前子树创建完后其子树的子树也是大根堆
    18. while(child//向下调整,要一路顺下去,保证是正确的
    19. if(child+11])){ //这里是看除了左孩子节点外,还有没有右孩子节点,如果有的话要找出来大的那个,以便之后替换
    20. child = child+1;
    21. }
    22. //还要判断该子树的根节点是不是最大的,是就直接break,因为已经是大根堆了,不是再交换变成大根堆
    23. if(arr[parent]>arr[child]){
    24. break;
    25. }else{
    26. int str = this.arr[parent];
    27. this.arr[parent] = this.arr[child];
    28. this.arr[child] = str;
    29. parent = child;
    30. child = parent * 2 + 1;
    31. }
    32. }
    33. }
    34. public static void main(String[] args) {
    35. TeatHeap teatHeap = new TeatHeap();
    36. teatHeap.creatHeap();
    37. System.out.print("这是堆排序前的堆:");
    38. for (int i = 0; i< teatHeap.arraySize; i++) {
    39. System.out.print(teatHeap.arr[i] + " ");
    40. }
    41. teatHeap.priorityQueueSort();
    42. System.out.println();
    43. System.out.print("这是堆排序后的堆:");
    44. for (int i = 0; i< teatHeap.arraySize; i++) {
    45. System.out.print("这是堆排序之后的堆" + teatHeap.arr[i] + " ");
    46. }
    47. }
    48. public void priorityQueueSort(){
    49. int end = arraySize-1;
    50. while(end>=0){
    51. int str = arr[0];
    52. arr[0] = arr[end];
    53. arr[end] = str;
    54. shiftDown(0,end); //这一步一定要放在end--前面,不然会到最后没有向下调整但是却换了位置了
    55. end--;
    56. }
    57. }
    58. }

     运行结果如下👇:


    以上!便是全部的啦😎

    又是收获满满的一天~

  • 相关阅读:
    【JavaWeb】之JSP
    ArcGIS基础:基于数据图框实现地理坐标系下不同投影转换的可视化效果
    python3-turtle(1)
    2022算法分析与练习十六
    Oracle Redo在线日志&Archive归档日志分析
    大算力时代已经到来
    数组不可以复制,导致了数组同样不支持传参
    通过HTTP发送大量数据的三种方法
    CSS--学习
    基于FPGA的VGA显示彩条、字符、图片
  • 原文地址:https://blog.csdn.net/Green_756/article/details/126730219