在学习完二叉树后,我们来学习优先级队列(堆)😎
所谓优先级队列,其实也就是一个按顺序存储方式来进行的完全二叉树,在一维数组中按照层序遍历的顺序存储。
堆的节点总是大于或者小于其子结点,且堆总是一棵完全二叉树
堆分为大根堆和小根堆。
⭕大根堆:大根堆的每个节点都大于其孩子节点。其图示如下👇
⭕小根堆:小根堆的每个节点都小于其孩子节点。其图示如下👇
以此我们可以知道,如果不是完全二叉树,那么数据不适合用数组来呈现,也就是不适合成为堆。
要了解一个数据结构,我们可以从仿写入手,显示创建一个堆(以大根堆为例)
首先我们有一个数组,里面存放着11,18,4,65,89,32,16这些数据。若要使得该数组变为大根堆,我们需要做到:从最后一个子树开始往前,直至最开始的树,每到一个树都向下调整。
🟢什么是向下调整?
向下调整即本次根节点及其子节点中,若是创建大根堆,即找到三个节点中的最大值,放到根节点的位置上;若是创建小根堆,即找到三个节点中的最小值,放到根节点的位置上。图示如下👇
在了解完什么是向下调整后,我们还有一些疑问:①要怎么样才可以找到最后一个子树;②在调整完当前子树后,怎么样走到下一棵子树?
🟢问题①:
我们要找到最后一颗子树,只需要运用之前在树中学习的知识,用(最后一个节点位置-1)/2 这个公式来求得最后一颗子树的根节点位置。
🟢问题②:
要走到下一个位置,我们知道这个是用数组的形式来存储的,使用只需要求得最后一颗子树位置后,该位置持续--就好。
🔴特别注意:需要特别注意的是,我们的向下调整指的是在调整完当前子树后,其子树的子树也是大根堆的状态
在解决完上面的疑问后,写出下面代码😎(具体操作见注释)
- public class TeatHeap {
-
- public int[] arr = {11,18,4,65,89,32,16}; //堆是以数组的方式存储的
-
- public void creatHeap(){
- //最后一棵子树的位置
- int root = (arr.length-1-1) / 2;
- while(root>=0){
- shiftDown(root); //一直向下调整直至调整到最后一颗树,也就是0位置上的整棵树
- root--;
- }
- }
-
- public void shiftDown(int root){
- int parent = root;
- int child = parent * 2 + 1; //孩子节点记得加一
- //这里使用while语句,是为了确保在当前子树创建完后其子树的子树也是大根堆
- while(child
//向下调整,要一路顺下去,保证是正确的 - if(child+1
arr[child+1])){ //这里是看除了左孩子节点外,还有没有右孩子节点,如果有的话要找出来大的那个,以便之后替换 - child = child+1;
- }
- //还要判断该子树的根节点是不是最大的,是就直接break,因为已经是大根堆了,不是再交换变成大根堆
- if(arr[parent]
- break;
- }else{
- int str = this.arr[parent];
- this.arr[parent] = this.arr[child];
- this.arr[child] = str;
- parent = child;
- child = parent * 2 + 1;
- }
- }
- }
在完成大根堆的创造后,创建小根堆也很容易,只需要将判断大小的代码child 改为childarr[child] 即可✨
🟢创建大根堆的时间复杂度
大根堆的创建的时间复杂度是O(n),具体的时间复杂度推算如下👇:
✨堆的插入
我们的堆在插入一个数据后要保证其还是大根堆或小根堆。
因为我们插入的数据放在数组的最后一个位置上,所以这最后的数据一定是孩子节点,为了使得堆为大根堆,我们需要从孩子节点开始,向上调整,就是为了保证每个孩子节点对应的子树都是大根堆,其原理与向下调整相似。具体代码如下👇:
- public boolean isFull(){
- return arraySize>=arr.length;
- }
-
- private void shiftUp(int child){
- int parent = (child-1) / 2;
- while(child>0){
- if(arr[child]>arr[parent]){
- int str = arr[child];
- arr[child] = arr[parent];
- arr[parent] = str;
- child = parent;
- parent = (child-1)/2;
- }else {
- break;
- }
- }
- }
-
- public void push(int val){
- if(isFull()){ //判断是不是满的数组,是就要扩容
- arr = Arrays.copyOf(arr, arr.length*2);
- }
- int child = arraySize; //在数组尾插入数据,之后开始向上调整
- arr[child] = val;
- shiftUp(child);
- arraySize++;
- }
✨堆的删除
堆的删除就是把堆的最上层元素与最后的元素对调,然后再重新从根开始,向下调整,便完成了删除操作。过程可参照下图👇:
具体代码如下👇:
- public boolean isEmpty(){
- return arraySize<=0;
- }
-
- public void pop(){
- if(isEmpty()){
- System.out.println("该堆为空");
- return;
- }
- int str = arr[0];
- arr[0] = arr[arraySize-1];
- arr[arraySize-1] = str;
- arraySize--;
- shiftDown(0);
- }
🌱优先级队列的应用—TopK问题、堆排序
✨TopK问题
请问我们要获得一个数组里面的前k个最大值,要怎么实现?
要解决上述问题,我们只需要将该数组变为一个大根堆,然后弹出一个后再变为大根堆,这样子弹出的便依次是第一个最大数、第二个最大数、第三个...
在这之前我们先需要创建一个大根堆,那么要怎么样才能够创建一个大根堆或是小根堆呢?具体代码和注释如下👇
- import java.util.Comparator;
- import java.util.PriorityQueue;
-
- class IntCmp implements Comparator
{ //写一个比较器,若o1-o2则是小根堆,o2-o1则是大根堆 - @Override
- public int compare(Integer o1, Integer o2) {
- return o2 - o1;
- }
- }
-
- public class demo1 {
- public static void main(String[] args) {
- IntCmp intCmp = new IntCmp();
- PriorityQueue
priorityQueue = new PriorityQueue<>(intCmp); //在这里放入比较器 - priorityQueue.offer(11);
- priorityQueue.offer(18);
- priorityQueue.offer(4);
- priorityQueue.offer(65);
- priorityQueue.offer(89);
- priorityQueue.offer(32);
- System.out.println("大怨种");
- }
- }
我们可以先把IntCmp里面的o2-o1改为o1-o2,此时创建的就会是小根堆,在下图的调试中可以看到👇:
然后把IntCmp里面的o1-o2改为o2-o1,此时创建的就会是小根堆,在下图的调试中可以看到👇:
在我们解决了大根堆小根堆的创建后,就只需要将元素pop出即可😎
但是其实这样的时间复杂度还是过于高了,原因是我们每弹出一个元素,就需要向下调整到最后。
于是,我们便有另一种思路:因为要获得前k个最大元素,我们只需要先创建一个小根堆,然后先将k个元素塞进去,之后就只关注这k个元素组成的堆,再在放入元素的时候先对堆顶元素对比,如果比堆顶元素大,则堆顶元素弹出,将新的元素放入,然后再从堆顶元素开始,向下调整,这样子就可以大大减少时间复杂度了😎
❗❗注意,如果要前k个最大数则要创建小根堆;求前k个最小值则需要创建大根堆❗❗
画图演示如下👇:
在画完图后,大家可以尝试一下自己写出代码,以下的博主写出的代码😎
- import java.util.Comparator;
- import java.util.PriorityQueue;
-
- class IntCmp implements Comparator
{ //写一个比较器,若o1-o2则是小根堆,o2-o1则是大根堆 - @Override
- public int compare(Integer o1, Integer o2) {
- return o1 - o2;
- }
- }
-
- public class demo1 {
-
- public static IntCmp intCmp = new IntCmp();
- public static PriorityQueue
priorityQueue = new PriorityQueue<>(intCmp); //在这里放入比较器 -
- public static void TopK(int[] arr,int k){
- for (int i = 0; i < k; i++) {
- priorityQueue.offer(arr[i]);
- }
- for (int i = k; i < arr.length; i++) {
- if(arr[i]>priorityQueue.peek()){
- priorityQueue.poll();
- priorityQueue.offer(arr[i]);
- }
- }
- for (int i = 0; i < k; i++) {
- System.out.println(priorityQueue.poll());
- }
- }
-
- public static void main(String[] args) {
- int[] arr = {11,18,4,32,89,65};
- TopK(arr,3);
- }
- }
在完成上面的所有代码后,我们就可以写LeetCode上的题目——最小K个数 😎
✨堆排序
堆排序就是将堆从大到小排序或者是从小到大排序。
我们堆排序的操作如下:假设创建从小到大排序,那么我们就需要创建一个大根堆,元素个数为k,将这个大根堆先向下调整,保证堆顶元素是最大的,然后将堆顶元素与最后一个元素位置调换,然后从下标为0开始(即堆顶位置),向下调整至k-2的位置,如果有两个元素已经从堆顶调换位置,那么就向下调整到k-3的位置,以此类推...这样我们就能保证每次将堆顶元素放到该排序的位置上了。具体图示如下👇
Tips:在这里我们将shiftDown函数稍作调整,变为两个形参,分别命名为int root和int end,以方便操作。并且要注意在调换完位置后,end--之前进行向下调整,否则会走到最后俩个元素的时候导致顺序不对
具体代码和注释如下👇:
- import java.util.Arrays;
- import java.util.PriorityQueue;
-
- public class TeatHeap {
-
- public int[] arr = {11,18,4,32,89,65};
- public int arraySize = arr.length;
-
- public void creatHeap(){
- //最后一棵子树的位置
- int root = (arr.length-1-1) / 2;
- while(root>=0){
- shiftDown(root,arr.length); //一直向下调整直至调整到最后一颗树,也就是0位置上的整棵树
- root--;
- }
- }
-
- public void shiftDown(int root,int end){
- int parent = root;
- int child = parent * 2 + 1; //孩子节点记得加一
- //这里使用while语句,是为了确保在当前子树创建完后其子树的子树也是大根堆
- while(child
//向下调整,要一路顺下去,保证是正确的 - if(child+1
1])){ //这里是看除了左孩子节点外,还有没有右孩子节点,如果有的话要找出来大的那个,以便之后替换 - child = child+1;
- }
- //还要判断该子树的根节点是不是最大的,是就直接break,因为已经是大根堆了,不是再交换变成大根堆
- if(arr[parent]>arr[child]){
- break;
- }else{
- int str = this.arr[parent];
- this.arr[parent] = this.arr[child];
- this.arr[child] = str;
- parent = child;
- child = parent * 2 + 1;
- }
- }
- }
-
- public static void main(String[] args) {
- TeatHeap teatHeap = new TeatHeap();
- teatHeap.creatHeap();
- System.out.print("这是堆排序前的堆:");
- for (int i = 0; i< teatHeap.arraySize; i++) {
- System.out.print(teatHeap.arr[i] + " ");
- }
- teatHeap.priorityQueueSort();
- System.out.println();
- System.out.print("这是堆排序后的堆:");
- for (int i = 0; i< teatHeap.arraySize; i++) {
- System.out.print("这是堆排序之后的堆" + teatHeap.arr[i] + " ");
- }
- }
-
- public void priorityQueueSort(){
- int end = arraySize-1;
- while(end>=0){
- int str = arr[0];
- arr[0] = arr[end];
- arr[end] = str;
- shiftDown(0,end); //这一步一定要放在end--前面,不然会到最后没有向下调整但是却换了位置了
- end--;
- }
- }
-
- }
运行结果如下👇:
以上!便是全部的啦😎
又是收获满满的一天~