【数据结构】用3500字学会优先级队列(堆)

news2024/11/26 0:35:45

文章目录

  • 💐1. 优先级队列
    • 1.1 概念
  • 💐2.堆的概念及存储方式
    • 2.1 什么是堆
    • 2.2 为什么要用完全二叉树描述堆呢?
    • 2.3 为什么说堆是在完全二叉树的基础上进行的调整?
    • 2.4 使用数组还原完全二叉树
  • 💐3. 堆的常用操作-模拟实现
    • 3.1 堆的创建
      • 3.1.1 堆的向下调整(大根堆为例)
      • 3.1.2 建堆的时间复杂度
    • 3.2 堆的插入和删除
      • 3.2.1 堆的插入
      • 3.2.2 堆的删除
  • 💐4. PriorityQueue常用接口及特性
      • PriorityQueue的构造
      • 优先级队列的扩容源码分析
  • 💐5. PriorityQueue的比较方式
  • 💐6. 例子:使用优先级队列解决TOP-k问题

💐1. 优先级队列

1.1 概念

队列是一种先进先出的数据结构,但是呢,有时候,数据之间也会有优先级,就比如说,我们想要拿到一个集合中的前k大的数据,或者是在平时,我们在打游戏时,这时候来电话了,但是,游戏也不能让他退出呀,这时候就会忽略掉电话,由此可见,游戏的优先级要比电话的优先级更高;

💐2.堆的概念及存储方式

优先级队列是由堆实现的而堆实际实际上是在完全二叉树的基础上进行了调整,而此时也就用到了 二叉树的顺序存储方式 这句话是什么意思呢?请看下图:

2.1 什么是堆

这里不做详细的介绍,只需知道,所有引用类型所创建的对象都保存在堆上,包括数组;而这里的堆是将所有的元素,按照完全二叉树的顺序存储方式存储到了一维数组中;

在这里插入图片描述

2.2 为什么要用完全二叉树描述堆呢?

在这里插入图片描述

2.3 为什么说堆是在完全二叉树的基础上进行的调整?

堆又分为了大根堆和小根堆

在这里插入图片描述

​ 从上图就可以发现两个性质:

1.大根堆中,所有的父节点都比子节点大

2.小根堆中,所有的父节点都比子节点小

2.4 使用数组还原完全二叉树

​ 在二叉树文章中,提到过这样一条性质:如果每一个节点都有一个编号 i 的话,那么:

1.当 i > 0 时,i 节点的父亲节点为: (i - 1) / 2;

2.如果 2i +1 < 节点的总数时,则下标为 i 节点的左孩子节点的下标为 2i + 1

3.如果 2i + 2 < 节点的总数时, 则下标为 i 的节点的左孩子节点的下标为 2i + 1

因为是在数组中进行存储的,所以完全二叉树中每一个节点的编号都是数组中每一个元素的下标;所以这样就可以使用数组来还原完全二叉树,然后再对完全二叉树进行调整;

在这里插入图片描述

在上一篇文章中,对于 优先级队列的概念及存储方式进行了一个详细的讲解,那么,本篇文章主要是针对优先级队列底层实现大致是什么样子的,亲手写一下代码结合图为大家讲解:

💐3. 堆的常用操作-模拟实现

3.1 堆的创建

我们知道,在上一篇文章中讲过,所有的元素都是在数组中存储的,那么,如何利用数组来实现 大根堆小根堆的创建呢?就要考虑下面这个问题:

如果出现子节点比父节点大或者小该怎么办呢,怎么去调整呢?

3.1.1 堆的向下调整(大根堆为例)

在这里插入图片描述

可以看出,最后一棵子树的子节点比父节点大,我们所用的方法就是:

在这里插入图片描述
在这里插入图片描述

代码实现:

public class MyPriorityQueue {
    //底层数组
    private int[] element;
    //数组中的元素
    private int usedSize;
    //初始默认容量
    private static final int default_capacity = 9;
    public MyPriorityQueue(int[] arr) {
        this.element = new int[default_capacity];
        //传入一个数组,对element进行构造
        for(int i = 0; i<arr.length; i++) {
            element[i] = arr[i];
            usedSize++;
        }
    }

    //建一个大根堆
    public void buildHeap() {
        //parent 求出最后一棵子树的父亲节点
        for(int parent = (usedSize-2)/2; parent >= 0; parent--) {
            /*
            为什么减2而不是减1呢?
            因为,如果是最后一个节点的下标值,就是减一,但是,数组的长度值比下标大1,所以减2
             */
            //向下调整
            shiftDown(parent, usedSize);
        }
    }
    private void shiftDown(int parent, int len) {
        //求左孩子节点
        int child = (2*parent)+1;

        //判断是否有右孩子节点并且判断左孩子节点是否大于右孩子节点
        if(child+1 < len && element[child] < element[child+1]){
            //得到最大的孩子节点
            child++;
        }
        //判断孩子节点是否比父亲节点大
        if(element[child] > element[parent]) {
            //进行交换
            swap(parent, child);
        }
    }
    private void swap(int parent, int child) {
        int tmp = element[parent];
        element[parent] = element[child];
        element[child] = tmp;
    }
    
        public static void main(String[] args) {
        //测试用例
        int[] ele = {50, 45, 40, 20, 25, 35, 30, 10, 60};
        MyPriorityQueue myPriorityQueue = new MyPriorityQueue(ele);
        myPriorityQueue.buildHeap();
    }

但是,上述代码存在一个致命的问题:

在这里插入图片描述

代码优化:

    private void shiftDown(int parent, int len) {
        //求左孩子节点
        int child = (2*parent)+1;
        while(child < len) {
            //判断是否有右孩子节点并且判断左孩子节点是否大于右孩子节点
            if(child+1 < len && element[child] < element[child+1]){
                //得到最大的孩子节点
                child++;
            }
            //判断孩子节点是否比父亲节点大
            if(element[child] > element[parent]) {
                //进行交换
                swap(parent, child);
                //保证交换后,该树的子树仍然是大根堆
                parent = child;
                child = (2*parent)+1;
            }else {
                //表示以该节点为根的树已经是大根堆了
                break;
            }
        }
    }

在这里插入图片描述

3.1.2 建堆的时间复杂度

在学会了建堆以后,接下来就聊一聊建堆所用的复杂度吧!先说结论,最坏的情况是O(n),下面我来推到以下:

在推导之前先说明一下:因为堆是完全二叉树,而满二叉树也是完全二叉树,多几个节点也无所谓,时间复杂度本来就是一个近似值,所以为了容易理解,这里会用满二叉树进行推导

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.2 堆的插入和删除

3.2.1 堆的插入

堆的插入分为两个步骤:

1.将要添加的元素放在底层数组的最后一个位置;注意是否要扩容问题

2.向上调整该元素

下面讲解一下什么是向上调整

直接拿要添加的元素与根节点相比较,因为,本身已经是大根堆了,直接与根节点比较,符合条件就交换,不需要再与其他的节点比较

在这里插入图片描述

代码实现:

    //插入方法
    public void offer(int val) {
        //判断是否需要扩容
        if(is_full()) {
            this.element = Arrays.copyOf(element, element.length+1);
        }
        //向上调整
        this.element[usedSize] = val;
        shiftUp();
        usedSize++;
    }

    //向上调整
    private void shiftUp() {
       int parent = (usedSize-1)/2;
       int child = usedSize;
       while(child > 0) {
           if(element[parent] < element[child]) {
               swap(parent, child);
           }
           child = parent;
           parent = (child-1)/2;
       }
    }
    private boolean is_full() {
        return usedSize == element.length;
    }

3.2.2 堆的删除

1.将第一个元素和最后一个元素进行交换

2.节点实际上并没有被删除,而是节点的个数useSize减1

3.最后向上调整

在这里插入图片描述

代码实现

    //删除
    public void poll() {
        //排除空堆情况
        if(usedSize == 0) {
            return;
        }
        //交换第一个和最后一个元素
        swap(0,usedSize-1);
        //节点个数减1,不会对最后一个元素进行判断
        usedSize--;
        for(int parent = (usedSize-1)/2; parent >= 0; parent--) {
            //向下调整
            shiftDown(parent, usedSize);
        }
    }

	private void shiftDown(int parent, int len) {
        //求左孩子节点
        int child = (2*parent)+1;
        while(child < len) {
            //判断是否有右孩子节点并且判断左孩子节点是否大于右孩子节点
            if(child+1 < len && element[child] < element[child+1]){
                //得到最大的孩子节点
                child++;
            }
            //判断孩子节点是否比父亲节点大
            if(element[child] > element[parent]) {
                //进行交换
                swap(parent, child);
                //保证交换后,该树的子树仍然是大根堆
                parent = child;
                child = (2*parent)+1;
            }else {
                //表示以该节点为根的树已经是大根堆了
                break;
            }
        }
    }

💐4. PriorityQueue常用接口及特性

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

PriorityQueue的构造

构造器功能讲解
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为intialCapacity的优先级队列,注意:initialCapacity不能小于1, 否则会抛出IllegalArgumentException异常
PriorityQueue(Collection<? extends E> c)用一个集合来创建优先级队列
PriorityQueue(Comparator<? super E> comparator)自定义类型进行比较,传入一个比较器

代码实现

   public static void main(String[] args) {
        //创建一个空的优先队列,底层默认容量是11
        PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();

        //创建一个空的优先级队列,指定容量为100
        PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(100);

        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        //使用其他集合构造优先级队列
        PriorityQueue<Integer> priorityQueue3 = new PriorityQueue<>(list);

        //默认是小根堆,想要变成大根堆需要传入比较器
        PriorityQueue<Integer> priorityQueue4 = new PriorityQueue<>(new com());
        priorityQueue4.offer(1);
        priorityQueue4.offer(2);
        priorityQueue4.offer(3);
    }
    //定义一个比较器
class com implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2-o1;
    }
}

PriorityQueue使用时需要注意:

1.使用时必须导入PriorityQueue所在的包,即:

import java.until.PriorityQueue;
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入不能比较大小的队象,否则会抛出ClassCastException异常
  2. 不能插入 null对象, 否则会抛出NullPointerException
  3. 没有容量限制,可以插入多个元素,其内部可以自动扩容
  4. 插入和删除元素的时间复杂度为O(log2N)
  5. PriorityQueue底层使用了堆数据结构
  6. PriorityQueue默认情况下是小根堆

Java中提供的方法:

函数功能介绍
boolean offer(E e)插入元素e, 插入成功返回true,如果e为空,抛出异常,空间不够时会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,放回null
E pool()移除优先级最高的元素,如果优先级队列为空,返回null
int size()获取有效元素个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,如果为空返回true

优先级队列的扩容源码分析

在这里插入图片描述
在这里插入图片描述

💐5. PriorityQueue的比较方式

PriorityQueue底层使用堆结构,所以,内部的元素必须能够比较大小,PriorityQueue采用了:Comparble 和 Comjparator两种方式。

  1. Comparble默认的内部比较方式,如果用户插入自定义类型对象是,该类对象必须要实现Comparble接口,并且要重写compareTo方法

  2. 也可以使用比较器,用户自己实现一个比较器类且实现Comaparator接口,并且让该类重写compare方法,指定根据对象的什么内容进行比较,然后再实例化PriorityQueue对象时,把比较器传进去;

源码讲解:

	//内部定义的比较器对象
    private final Comparator<? super E> comparator;

	//如果用户没有提供比较器,则使用内部的比较方式,将comparator置为null
	public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

	//如果用户提供了比较器,则采用提供的比较器进行比较
	public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // Note: This restriction of at least one is not actually needed,
        // but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

	//在添加对象时进行向上调整
	//如果没有提供比较器,则采用内部比较方式,即Comparable
	//如果提供了比较器,则采用比较器进行比较
	 private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

	//采用comparable进行比较
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }
	//采用comparator进行比较
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

在这里插入图片描述

在这里插入图片描述

💐6. 例子:使用优先级队列解决TOP-k问题

TOP-K 问题:求数据集合中前K个最大的元素或者最小的元素,一般数据量都比较大;

[面试题 17.14. 最小K个数 - 力扣(LeetCode)]()

class Solution {
    public int[] smallestK(int[] arr, int k) {
        //创建一个优先级队列
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        //将所有元素加入到队列中
        for(int i = 0; i<arr.length; i++) {
            heap.add(arr[i]);
        }
        //创建一个数组用来存储前k个元素
        int[] ans = new int[k];
        //将堆中的前k个元素保存在数组中
        for(int i = 0; i<k; i++) {
            ans[i] = heap.poll();
        }
        //返回数组
        return ans;
    }
}

但是,上面这个代码只能针对于数据量较小时才行,如果数据量太大就会造成时间复杂度过高,比如,有一亿个数据,如果使用以上代码的话,会先将一亿个数据都保存在堆中,然后进行比较,这就得不偿失了;那么就要对代码进行一个优化:

1.用集合中的前k个元素建堆;

  • 前k大个元素建小堆
  • 前k小个元素建大堆

2.用剩余的n(元素的个数) - k个元素与堆中的元素进行比较;最后堆中剩余的元素就是前k个最大或最小的元素

代码实现:

    public int[] smallestK(int[] arr, int k) {
        //求前k个最小元素,所以要建大根堆,因为PriorityQueue默认的是小根堆,所以要传入比较器
        PriorityQueue<Integer> heap = new PriorityQueue<>(new Com());
        //将前k个元素加入到堆中
        for(int i = 0; i<k; i++){
            heap.add(arr[i]);
        }
        if(heap.isEmpty()) {
            return new int[]{};
        }
        //用剩余的元素与堆顶元素比较
        for(int i = k; i<arr.length; i++) {
            int x = heap.peek();
            //这里的条件语句求的是前k个最小元素
            if(arr[i] < x){
                heap.poll();
                heap.offer(arr[i]);
            }
        }

        int[] ans = new int[k];
        for(int i = 0; i<k; i++){
            ans[i] = heap.poll();
        }
        return ans;
    }
}
//自定义一个比较器
class Com implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1014919.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【CMU 15-445】Proj0 C++ Primer

C Primer Task1 Copy-On-Write TrieTask2 Concurrent Key-Value StoreTask3 DebuggingTask4 SQL String Functions CMU-15445汇总 本文对应的project版本为CMU-Spring-2023的project0 默认读者已经学会了字典树Trie Task1 Copy-On-Write Trie Task1要求实现一个可持久化字典树…

邀请加入团队

**将地址发送给同团队的人克隆失败 ** —解决办法 邀请加入团队 在这里插入图片描述

港科夜闻|香港科大2020十大准独角兽开思完成2亿元D2,D3轮融资,推进汽后全产业链数字化转型...

关注并星标 每周阅读港科夜闻 建立新视野 开启新思维 1、“香港科大2020十大准独角兽”开思完成2亿元D2,D3轮融资&#xff0c;推进汽后全产业链数字化转型。开思一直致力于为汽车后市场打造数字化基础设施&#xff0c;建设行业服务标准与信用体系&#xff0c;其业务涵盖一站式汽…

Hive内置函数字典

写在前面&#xff1a;HQL同SQL有很多的类似语法&#xff0c;同学熟悉SQL后一般学习起来非常轻松&#xff0c;写一篇文章列举常用函数&#xff0c;方便查找和学习。 1. 执行模式 1.1 Batch Mode 批处理模式 当使用-e或-f选项运行$ HIVE_HOME / bin / hive时&#xff0c;它将以…

【ChatGPT原理与实战】4个维度讲透ChatGPT技术原理,揭开ChatGPT神秘技术黑盒!

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;陈童学哦&#xff0c;目前学习C/C、算法、Python、Java等方向&#xff0c;一个正在慢慢前行的普通人。 &#x1f3c0;系列专栏&#xff1a;陈童学的日记 &#x1f4a1;其他专栏&#xff1a;CSTL&…

三个激活函数在同一figure上的实现

######后期会更新 import matplotlib.pyplot as plt from matplotlib.pyplot import MultipleLocator import numpy as np import mathplt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False# sigmoid激活函数用的最少&#xff0c;但为二分类问题…

零基础VB6无壳P-CODE逆向分析(VB Decompiler应用与避坑)

> 前言 最近从朋友那里拿到了一个加密狗授权的软件安装包,秉承着LCG的精神,开启了逆向之路,经过查壳和综合分析确定是VB6编写的程序(这年头使用VB6开发商业程序的还真少见),作为一名C# Winform的业余程序员,靠着C#的知识勉强分析个大概. > 授权简介 软件共分三种授权模…

Flex布局详解

目录 一、Flex 布局是什么&#xff1f; 二、基本概念 三、容器的属性 3.1 flex-direction属性 3.2 flex-wrap属性 3.3 flex-flow 3.4 justify-content属性 3.5 align-items属性 3.6 align-content属性 四、项目的属性 4.1 order属性 4.2 flex-grow属性 4.3 flex-s…

基于Python+Flask实现一个TODO任务管理系统网站

随着科技的进步&#xff0c;数字化的任务清单逐渐成为生活中不可或缺的一部分。它们不仅可以帮助我们跟踪日常任务&#xff0c;还可以提高效率。但是&#xff0c;你是否考虑过自己制作一个任务管理系统呢&#xff1f; 好消息是&#xff0c;使用Python和Flask&#xff0c;我们可…

数据库安全(Mysql,Hadoop,Redis)

MySQL Mysql 身份认证绕过漏洞&#xff08;CVE-2012-2122&#xff09; 当连接MariaDB/MySQL时&#xff0c;输入的密码会与期望的正确密码比较&#xff0c;由于不正确的处理&#xff0c;会导致即便是memcmp()返回一个非零值&#xff0c;也会使MySQL认为两个密码是相同的。也就…

C++:初始化列表,static成员,友元,内部类

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》 文章目录 前言一、初始化列表二、static成员三、友元四、内部类总结 前言 本篇博客作为C&#xff1a;初始化列表&#xff0c;static成员&#xff0c;友元&#xff0c;内部类的知识总结。 一…

js实现websocket服务端和客户端

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

城市社交活动系统+附近交友资源类短视频APP源码

城市社交活动系统附近交友资源类短视频&#xff0c;注意只有安卓端源码&#xff01;

Python中Mock和Patch的区别

前言&#xff1a; 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 在测试并行开发&#xff08;TPD&#xff09;中&#xff0c;代码开发是第一位的。 尽管如此&#xff0c;我们还是要写出开发的测试&#xff0c…

【JavaSE笔记】数据类型与变量

一、前言 在Java这门“啰嗦”的编程语言中,我们必须弄清楚每种数据类型的性质和用途,才能让程序“说人话”。要成为Java高手&#xff0c;就必须与各种数据类型打成一片。 本文则将带你认识Java中常见的两位“角色”—数据类型与变量。 二、数据类型 在Java中数据类型主要分…

leetcode刷题(简单篇):9.回文数

9.回文数 题目描述&#xff1a;给你一个整数 x &#xff0c;如果 x 是一个回文整数&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 回文数是指正序&#xff08;从左向右&#xff09;和倒序&#xff08;从右向左&#xff09;读都是一样的整数。 例如&…

怒刷LeetCode的第2天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;滑动窗口 方法二&#xff1a;双指针加哈希表 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;二分查找 方法二&#xff1a;归并排序 方法三&#xff1a;分治法 第三题 题目来源 题目内容 解…

理解HTTPS/TLS/SSL(二)可视化TLS握手过程并解密加密数据

文章目录 WireShark抓包TLS握手过程Client HelloServer HelloEncryped Extenstions, Certificate, Certificate VerifyChange Ciper Spec, FinshedTLS 1.2和TLS 1.3的区别能不能在进一步&#xff1f; 解密WireShark中抓到的TLS包参考资料 上一篇文章已经在本地使用了生成自签名…

从字符串中删除指定字符

任务描述 编写一个函数实现功能&#xff1a;从字符串中删除指定的字符。同一字母的大、小写按不同字符处理。例如&#xff1a;程序执行时输入字符串&#xff1a;turbo c and Borland c&#xff0c;从键盘输入字符n&#xff0c;则输出后变为&#xff1a;turbo c ad Borlad c。如…