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

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

🌱优先级队列(堆)定义

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

✨堆的性质

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

✨堆的种类

堆分为大根堆和小根堆。

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

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

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


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

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

创建大根堆 

首先我们有一个数组,里面存放着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<arr.length){    //向下调整,要一路顺下去,保证是正确的
            if(child+1<arr.length&&(arr[child]>arr[child+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;
            }
        }
    }

 在完成大根堆的创造后,创建小根堆也很容易,只需要将判断大小的代码child<arr.length&&arr[parent]<arr[child]改为child<arr.length&&arr[parent]>arr[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<Integer> {   //写一个比较器,若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<Integer> 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<Integer> {   //写一个比较器,若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<Integer> 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<end){    //向下调整,要一路顺下去,保证是正确的
            if(child+1<end&&(arr[child]<arr[child+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--;
        }
    }

}

 运行结果如下👇:


以上!便是全部的啦😎

又是收获满满的一天~

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java中的队列是一种基本的数据结构,用于存储和管理元素。Queue接口是Java集合框架中定义的一个接口,它提供了一组方法来操作队列。不同的实现类可以选择不同的方式来实现队列的功能。 在Java中,我们可以使用LinkedList类实现Queue接口来创建一个队列对象。通过使用add()方法向队列中添加元素,使用peek()方法获取队列的头部元素,使用remove()方法移除队列的头部元素,使用size()方法获取队列的大小,使用isEmpty()方法判断队列是否为空。以下是一个使用Queue接口和LinkedList实现类的示例: import java.util.Queue; import java.util.LinkedList; public class QueueExample { public static void main(String[] args) { // 创建一个Queue对象 Queue<String> queue = new LinkedList<>(); // 添加元素到队列 queue.add("Apple"); queue.add("Banana"); queue.add("Orange"); // 获取队列头部元素 String head = queue.peek(); System.out.println("头部元素:" + head); // 遍历队列并输出元素 System.out.println("队列元素:"); for (String element : queue) { System.out.println(element); } // 移除队列头部元素 String removedElement = queue.remove(); System.out.println("移除的元素:" + removedElement); // 队列大小 int size = queue.size(); System.out.println("队列大小:" + size); // 判断队列是否为空 boolean isEmpty = queue.isEmpty(); System.out.println("队列是否为空:" + isEmpty); } } 除了Queue接口,Java中还提供了另一个接口叫做Deque(Double Ended Queue,双端队列),它提供了在队列的两端进行操作的方法。同样地,我们可以选择不同的实现类来创建一个双端队列对象。以下是一个使用Deque接口和ArrayDeque实现类的示例: import java.util.Deque; import java.util.ArrayDeque; public class DequeExample { public static void main(String[] args) { // 创建一个Deque对象 Deque<String> deque = new ArrayDeque<>(); // 添加元素到双端队列 deque.addFirst("Apple"); deque.addLast("Banana"); deque.addLast("Orange"); // 获取双端队列头部和尾部元素 String first = deque.getFirst(); String last = deque.getLast(); System.out.println("头部元素:" + first); System.out.println("尾部元素:" + last); // 遍历双端队列并输出元素 System.out.println("双端队列元素(从头到尾):"); for (String element : deque) { System.out.println(element); } // 移除双端队列头部和尾部元素 String removedFirst = deque.removeFirst(); String removedLast = deque.removeLast(); System.out.println("移除的头部元素:" + removedFirst); System.out.println("移除的尾部元素:" + removedLast); // 双端队列大小 int size = deque.size(); System.out.println("双端队列大小:" + size); // 判断双端队列是否为空 boolean isEmpty = deque.isEmpty(); System.out.println("双端队列是否为空:" + isEmpty); } } 除了Queue接口和Deque接口,Java还提供了优先队列(PriorityQueue)的实现类。优先队列根据元素的优先级进行排序,在插入元素时会按照一定的规则来确定元素的位置。以下是一个使用优先队列的示例: import java.util.PriorityQueue; import java.util.Comparator; public class PriorityQueueExample { public static void main(String[] args) { // 创建一个优先队列对象,默认采用最小实现 PriorityQueue<Integer> queue = new PriorityQueue<>(10, new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; // 如果a>b则交换,所以这是一个递增序列 } }); // 向优先队列中添加元素 queue.offer(13); queue.offer(9); // 遍历优先队列并输出元素 int len = queue.size(); for (int i = 0; i < len; i++) { System.out.println(queue.poll()); } // 输出 9 13 // 默认采用最小实现的优先队列 PriorityQueue<Integer> queue2 = new PriorityQueue<>(); queue2.offer(11); queue2.offer(9); // 遍历优先队列并输出元素 len = queue2.size(); for (int i = 0; i < len; i++) { System.out.println(queue2.poll()); } // 输出 9, 11 } } 此外,我们还可以使用Java数组来实现一个简单的队列,但这种方式需要手动实现队列的各种操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值