【数据结构】优先级队列(堆)全面讲解

兄弟们,好久不见!!!相信你们一定想我了,但是,我更想你们。作为补偿,接下来要给大家带来更好更易懂的内容,一起交流一起进步鸭!老铁们可以互关哦!那么让我们出发吧!
在这里插入图片描述

什么是堆

  1. 优先级队列是一个类,即PriorityQueue,其底层最主要的是继承了AbstractQueue,而AbstractQueue又实现了Queue接口在这里插入图片描述
    在这里插入图片描述

  2. 优先级队列往往用堆来实现,堆,也即heap

  3. 堆逻辑上是一棵完全二叉树,物理上是保存在数组上,即其底层是一棵顺序存储的完全二叉树,一般只适合保存完全二叉树,因为非完全二叉树会有空间的浪费(中间不是连续的,自然会浪费空间)

补:完全二叉树在这里插入图片描述
完全二叉树(Complete Binary Tree)

  1. 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
  1. 完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
  1. 一棵二叉树至多只有最下面的一层上的结点的度数可以小于2,并且最下层上的结点都集中在该层最左边的若干位置上,而在最后一层上,右边的若干结点缺失的二叉树,则此二叉树成为完全二叉树。

简单的来说,可以把完全二叉树理解为从上到下从左到右依次放节点,中间不能间断,如果每层都放满,则是满二叉树,因此,完全二叉树又是一种特殊的满二叉树

堆的基本概念与性质

基本概念

  1. 满足任意节点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆.
    在这里插入图片描述

  2. 满足任意结点的值都小于其子树中结点的值,叫做小堆,或者小根堆,或者最小堆.
    在这里插入图片描述

注意:不管是大根堆还是小根堆,左右孩子的大小关系是不确定的,我们只能确定根节点和孩子节点的关系

下标关系

  1. 已知双亲(parent)的下标,则:
    左孩子(left)下标 = 2 * parent + 1;
    右孩子(right)下标 = 2 * parent + 2;
  2. 已知孩子(不区分左右)(child)下标,则:
    双亲(parent)下标 = (child - 1) / 2;

作用

在了解了堆一般是分为大堆或者小堆后,我们就很容易地知道了如果在求最值时,即最大值,最小值,前K个最大,前K个最小,第K大,第K小等问题时可以用到堆

创建堆的方法

具体实现思路及代码

下面我们给出一个数组,这个数组逻辑上可以看做一棵完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?我们以创建大堆为例,调整方法为从最后一棵子树的根节点开始,向下调整,找左右孩子的最大值,然后最大值和根换,每一棵树都是这么调整的,这种方法叫做向下调整
例:

int[] array = { 27,15,19,18,28,34,65,49,25,37 };

在这里插入图片描述
此数组调整前用完全二叉树表示的形式是这样的,然后我们通过代码来和大家一块调整!在这里插入图片描述
Heap部分

import java.util.Arrays;
public class Heap {
    public int[] elem;
    public int usedSize;
    public Heap() {
        this.elem = new int[10];
    }
    public void createHeap(int[] array) {

        for (int i = 0; i < array.length; i++) {
            this.elem[i] = array[i];
            this.usedSize++;
        }

        //parent 就代表每颗子树的根节点
        for(int parent = (array.length-1-1)/2;parent >= 0;parent--) {
            //第2个参数  每次调整的结束位置应该是:this.usedSize.
            adjustDown(parent,this.usedSize);
        }
    }

    public void adjustDown(int root,int len) {
        int parent = root;
        int child = 2*parent+1;
        while(child < len) {
            //找到左右孩子的最大值
            //1、前提是你得有右孩子
            if(child+1 < len && this.elem[child] < this.elem[child+1]) {
                child++;
            }
            //保证,child下标的数据  一定是左右孩子的最大值的下标
            if(this.elem[child] > this.elem[parent]) {
                int tmp = this.elem[child];
                this.elem[child] = this.elem[parent];
                this.elem[parent] = tmp;
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

}

测试类部分

import java.util.Arrays;
import java.util.PriorityQueue;

public class TestDemo {
    public static void main(String[] args) {
        int[] array = { 27,15,19,18,28,34,65,49,25,37 };
        Heap heap = new Heap();
        heap.createHeap(array);
        System.out.println(Arrays.toString(heap.elem));
    }
}

此处注意了,我们在测试类中创建的数组名是array,而实际调整的数组是elem,因此我们打印elem的结果即可看到我们调整为大堆的结果了:
在这里插入图片描述
实际效果图如下:在这里插入图片描述
怎么样,是不是很简单呢?

时间复杂度分析(重难点)

时间复杂度通常指最坏的情况下处理问题所需的时间,在这里最坏的情况即为满二叉树的情况,这样每棵树都要进行调整
在这里插入图片描述
下面我们利用公式来计算:总的时间复杂度即为每层每个节点要调整的高度的和,即:在这里插入图片描述
我们给等式两边同时乘以2,利用错位相减来求:在这里插入图片描述
计算的结果为:在这里插入图片描述
然后我们用等比数列的求和公式计算:在这里插入图片描述
结果:在这里插入图片描述
然后,我们需要对此式进行进一步地化简!在这里插入图片描述
由于此树是一棵满二叉树,所以我们设一共有n个节点,此树的高度为h,那么可得公式:n= 2 h 2^h 2h-1,化简得h=log₂(n + 1),代入原式可得T(n) = n - log₂(n + 1)在这里插入图片描述

由对数曲线可得,当n逐渐变大时,其结果逐渐趋于一个常数,所以可得时间复杂度为n

堆的应用

概念

在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次高的对象。最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)

原理

优先级队列的实现方式有很多,但最常见的是使用堆来构建。

入队列

我们以大堆为例

  1. 首先按尾插方式放入数组
  2. 比较其和其双亲的值的大小,如果双亲的值大,则满足堆的性质,插入结束,此调整法为向上调整,即让要调整的节点与父亲节点比较
  3. 否则,交换其和双亲位置的值,重新进行 2、3 步骤
  4. 直到根结点在这里插入图片描述

具体代码如下:

 public void adjustUp(int child) {
        int parent = (child-1)/2;
        while (child > 0) {
            if(this.elem[child] > this.elem[parent]) {
                int tmp = this.elem[parent];
                this.elem[parent] = this.elem[child];
                this.elem[child] = tmp;
                child = parent;
                parent = (child-1)/2;
            }else {
                break;
            }
        }
    }

    public void push(int val) {
        if(isFull()) {
            //扩容
            this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
        }
        this.elem[this.usedSize] = val;//10
        this.usedSize++;//11
        adjustUp(this.usedSize-1);//10下标
    }

    public boolean isFull() {
        return this.usedSize == this.elem.length;
    }

代码写完了,让我们测试下吧!

import java.util.Arrays;

public class TestDemo {
    public static void main(String[] args) {
        int[] array = { 27,15,19,18,28,34,65,49,25,37 };
        Heap heap = new Heap();
        heap.createHeap(array);
        heap.push(55);
        System.out.println(Arrays.toString(heap.elem));
    }
}

输出结果
在这里插入图片描述
可以看出,我们成功建好了大堆并进行了扩容。

出队列

为了防止破坏堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整方式重新调整成堆

在这里插入图片描述
具体代码如下:

public void pop() {
        if(isEmpty()) {
            return;
        }
        int tmp = this.elem[0];
        this.elem[0] = this.elem[this.usedSize-1];
        this.elem[this.usedSize-1] = tmp;
        this.usedSize--;//9 删除了
        adjustDown(0,this.usedSize);
    }
    public boolean isEmpty() {
        return this.usedSize == 0;
    }

同样,让我们测试一下吧!在这里插入图片描述
大功告成!

返回队首元素

这个很简单,只需直接返回即可:

public int peek() {
        if(isEmpty()) {
           throw new RuntimeException("队列为空");
        }
        return this.elem[0];
    }

Java当中的优先级队列

  1. Java当中的优先级队列为PriorityQueue,底层是一个堆,默认容量是11
public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {

    private static final long serialVersionUID = -7720805057305804111L;

    private static final int DEFAULT_INITIAL_CAPACITY = 11;
      transient Object[] queue; // non-private to simplify nested class access

在这里插入图片描述
实例化时默认调用无参的构造方法,而无参的构造方法又会调用带有两个参数的构造方法,即默认会调用带有两个参数的构造方法

  1. 那么,默认会建小堆还是大堆呢?让我们测试下:
 PriorityQueue<Integer> priorityQueue=new PriorityQueue<>();
        priorityQueue.offer(1);
        priorityQueue.offer(53);
        priorityQueue.offer(33);
        System.out.println(priorityQueue);

打印结果为:
在这里插入图片描述
即:在这里插入图片描述
可以看出默认会创建小堆。

  1. 错误处理:
错误处理抛出异常返回特殊值
入队列add(e)offer(e)
出队列remove()poll()
队首元素element()peek()

TopK问题(重难点)

什么是TopK问题?简单的来说,就是在一个很大的数据里找到前K大或者前K小的数,这个问题也是十分经典的算法问题,不论是面试中还是实际开发中,都非常典型,下面我会以简单易理解的方式带大家学会

在这里插入图片描述

排序

关于此类问题,相信绝大部分人的第一反应就是用Arrays类自带的函数sort进行排序,然后取前K个,此方法不是不行,但是不容易在面试中脱颖而出,只有在实在没有其他办法的情况下,才可以先凑合着把这个方法写上
在这里插入图片描述
时间复杂度:O(n*lg(n))

排序的优化

该方法与第一个排序方法类似,用一个容器保存前K个数,然后将剩余的所有数字——与容器内的最小数字相比,若所有后续的元素都比容器内的K个数还小,那么容器内的K个数就是最大K个数。如果某一后续元素比容器内最小数字大,就删掉容器内最小元素,并将该元素插入容器,最后遍历完所有的数,得到的结果容器中保存的数即为最终结果了。
时间复杂度:O(n+m^2)(m为K的大小)

利用堆

此方法利用堆的特性来解,思路为维护一个大小为 K 的小顶堆,依次将数据放入堆中,当堆的满了的时候,将堆顶元素与下一个数比较:如果大于堆顶元素,则将当前的堆顶元素抛弃,并将该元素插入堆中。遍历完全部数据,要求的前K个最大的元素也自然都在堆里面了。
在这里插入图片描述
首先将数据插入堆,维持一个小顶堆
在这里插入图片描述
因为85大于堆顶元素,所以进行替换在这里插入图片描述
继续维持小顶堆

注意

  1. 若要求前K个最小,只需变成大顶堆即可
  2. 遍历完全部数据后,堆顶元素即为第K大的数据

时间复杂度:O(nlogK)

实现代码如下:

public static void topk(int[] array,int k) {
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });
        //大堆
        for (int i = 0; i < array.length; i++) {
            if(maxHeap.size() < k) {
                maxHeap.offer(array[i]);
            }else {
                int top = maxHeap.peek();
                if(top > array[i]) {
                    maxHeap.poll();
                    maxHeap.offer(array[i]);
                }
            }
        }
        System.out.println(maxHeap);
    }

接下来我们来测试一下吧!假如我们要求前3个最小元素:

 int[] array = {1,25,2,10,5,35,21,19,56};
        topk(array,3);

结果如下:
在这里插入图片描述
成功了!!!是不是很简单?相信你只要认真多看看肯定会明白的呢在这里插入图片描述

堆排序(重难点)

  1. 堆排序是利用堆这种数据结构设计出的一种排序算法,其是选择排序的一种,它利用大顶堆(小顶堆)堆顶元素是最大值(最小值)这一特性,使得每次从无序中选择最大值(最小值)变得简单。
  2. 排升序要建大堆;排降序要建小堆。

具体步骤如下
step1:先将带排序的数组构造成一个大根堆,假设有如下数组:int[] array2={2,3,4,1,6,5};

在这里插入图片描述
构造成大根堆如下:
在这里插入图片描述

step2:将堆顶元素与堆尾元素交换:在这里插入图片描述
step3:将除6以外其他的所有元素继续构造大根堆:在这里插入图片描述
以此类推,然后再将堆顶元素与堆中倒数第二个元素交换,换完之后除了倒数第一个和倒数第二个元素以外,其他元素继续构造成大堆,最终会得到有序的数组
在这里插入图片描述
同理,如果要从大到小排,则构建小堆即可!

public static void siftDown(int[] array,int root,int len) {
        int parent = root;
        int child = 2*parent+1;
        while (child < len) {
            //找到左右孩子的最大值
            //1、前提是你得有右孩子
            if(child+1 < len && array[child] < array[child+1]) {
                child++;
            }
            //child的下标就是左右孩子的最大值下标
            if(array[child] > array[parent]) {
                int tmp = array[child];
                array[child] = array[parent];
                array[parent] = tmp;
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }

    public static void createHeap(int[] array) {
        //从小到大排序 -》 大根堆
        for (int i = (array.length-1 - 1) / 2;  i >= 0 ; i--) {
            siftDown(array,i,array.length);
        }
    }
    public static void heapSort(int[] array) {
        createHeap(array);//O(n)
        int end = array.length-1;
        while (end > 0) {//O(N*logN)
            int tmp = array[end];
            array[end] = array[0];
            array[0] = tmp;
            siftDown(array,0,end);
            end--;
        }
    }

  1. 时间复杂度:O(n * log(n))(最好和最坏的都是这个)
  2. 空间复杂度:O(1)(在整个调整的过程中并没有重新定义数组)
  3. 稳定性:不稳定

那么,优先级队列的全部要分享的内容都已经给大家整理完啦,只要大家能更好地理解,博主就很开心了。期待以后给大家分享更多有趣易懂的知识,然后我们一起加油!另外,有什么建议可以随时提哦~在这里插入图片描述

  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 33
    评论
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

春风~十一载

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值