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

一、优先级队列(PriorityQueue)

1、概念

队列是一种先进先出(FIFO)的数据结构,但是有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,在这种情况下使用队列就不行了,比如玩王者的时候突然女朋友一通电话,游戏屏幕瞬间被电话占领,这时候就应该优先处理电话。

在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新对象,这种数据结构就是优先级队列(PriorityQueue)

PriorityQueue 的底层是堆,堆的底层是数组,在文章后面有详细描述

2、PriorityQueue特性

Java集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,这里主要使用PriorityQueue。

3、PriorityQueue使用的注意点

  • 使用时必须导入 PriorityQueue 所在的包
import java.util.PriorityQueue
  • PriorityQueue中放置的元素必须要能够比较大小 (只有实现了 Comparable 和 Comparator 接口的类才能比较大小),不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常
  • 不能插入 null 对象,否则会抛出 NullPointerException 异常
  • 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  • 插入和删除元素的时间复杂度均为 O(log2N)
  • PriorityQueue底层使用了堆数据结构

4、常用接口介绍

4.1、 优先级队列的构造

这里只是列举了常见的几种构造方式

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为 initialCapacity 的优先级队列。注意:initialCapacity 不能小于1,否则会抛出 IllegalArgumentException 异常
PriorityQueue(Collection <? extends E> c)用一个集合来创建优先级队列

PriorityQueue当中,最小的元素就是优先级最高的元素

static void PriorityQueueDemo() {

        //1、创建一个空的优先级队列,默认底层容量是11
        PriorityQueue<Integer> queue1 = new PriorityQueue<>();

        //2、创建一个空的优先级队列,底层的容量是 initialCapacity
        PriorityQueue<Integer> queue2 = new PriorityQueue<>(50);

        ArrayList<Integer> list = new ArrayList<>();
        list.add(4);
        list.add(0);
        list.add(2);
        list.add(3);

        //3、用 ArrayList 集合来创建一个优先级队列的对象
        PriorityQueue<Integer> queue3 = new PriorityQueue<>(list);
        System.out.println(queue3.size());
        System.out.println(queue3.peek());
    }

运行结果:

4
1
4.2、插入/删除/获取优先级最高的元素
函数名功能介绍
boolean offer(E e)插入元素 e,插入成功返回 true,如果 e 对象为空,抛出 NullPointerException 异常,时间复杂度为 O(log2N) ,注意:空间不够时会自动扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回 null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回 null
int size()获取有效元素的个数
void clean()清空
boolean isEmpty()检测优先级队列是否为空,空返回 true

补充:
在这里插入图片描述

5、扩容

jdk1.8 中,PriorityQueue的扩容方式:
可以在手册中搜索 grow() 函数

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        
        int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2): (oldCapacity >> 1));

        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            newCapacity = hugeCapacity(minCapacity);
        }

        queue = Arrays.copyOf(queue,newCapacity);
    }

优先队列的扩容说明:

  • 如果容量 < 64,按照 oldCapacity * 2 + 2 进行扩容
  • 如果容量 >= 64,按照 oldCapacity * 1.5 进行扩容
  • 如果容量超过 MAX_ARRAY_SIZE,按照 MAX_ARRAY_SIZE 进行扩容

6、优先级队列的应用

topK 问题

topK-LeetCode

在这里插入图片描述

思路:将数组所有元素放到优先级队列当中,然后取前 K 个

class Solution {
    public int[] smallestK(int[] arr, int k) {
    
        int[] ret = new int[k];

        if (arr == null || k <= 0) {
            return ret;
        }
        
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        for (int i = 0; i < arr.length; i++) {
            queue.offer(arr[i]);
        }
        
        for (int i = 0; i < k; i++) {
            ret[i] = queue.poll();
        }

        return ret;
    }
}

二、堆(Heap)

前提知识:二叉树的顺序存储

使用数组存储二叉树的方式,就是将二叉树按照层序遍历放入数组
一般只适合完全二叉树,因为非完全二叉树会有空间的浪费
这种方式的主要用法就是堆的表示
在这里插入图片描述

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

1、概念

概括:堆就是一颗顺序存储的完全二叉树,底层是一个数组

  1. 堆逻辑上是一颗完全二叉树

  2. 堆物理上是保存在数组中

  3. 堆满足任意结点的值都大于其子树中结点的值,也就是所有根节点 > 其左右孩子结点,叫做大堆,或者大根堆、最大堆

  4. 反之则是小堆,或者小根堆、最小堆
    在这里插入图片描述

  5. 堆的基本作用是快速找到集合中的最值

2、性质

  • 堆中某个节点的值总是不大于或不小于其父结点的值
  • 堆总是一颗完全二叉树

3、向下调整

找左右孩子最大值,然后和父亲结点进行交换

  • 代码:
public class TestHeap {
    public int[] elem;
    public int usedSize;

    public TestHeap() {
        this.elem = new int[10];
        this.usedSize = 0;
    }

	/*
		code here
	*/
}

	/**
     * 向下调整
     * @param root 每棵子树根节点
     * @param len 每棵子树结束位置
     */
    public void adjustDown(int root,int len) {
        int parent = root;
        int child = 2*root + 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;
            }
        }
    }
  • 时间复杂度分析:
    最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度即时间复杂度为O(log(n))

4、建堆

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。

具体做法就是,从最后一个非叶子结点子树开始,比较左右孩子结点,较大的孩子结点和父亲结点比较,比父亲结点大的话就进行交换,直到这棵子树已经成了一个堆

  • 图示(以大根堆为例):
// 建堆前
int[] array = { 1,5,3,8,7,6 };
// 建堆后
int[] array = { 8,7,6,5,1,3 };

在这里插入图片描述

  • 时间复杂度分析:

粗略估算,可以认为是在循环中执行向下调整,为 O(n * log(n))(了解),实际上是 O(n)

  • 代码:
	/**
     * 向下调整
     * @param root 每棵子树根节点
     * @param len 每棵子树结束位置
     */
    public void adjustDown(int root,int len) {
        int parent = root;
        int child = 2*root + 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;
            }
        }
    }
    
    /**
     * 建堆
     * @param array 传入的数组
     **/
    public void creatHeap (int[] array) {
        for (int i = 0; i < array.length; i++) {
            this.elem[i] = array[i];
            this.usedSize++;
        }
        //i代表每颗子树根结点
        for (int i = (this.usedSize - 1 - 1) / 2; i >= 0 ; i--) {
            adjustDown(i,this.usedSize);
        }
    }

要将一棵树调整为大根堆或者小根堆,方法就是 : 从这棵树的最后一个子树进行向下调整,每一颗子树都要进行向下调整

  • 时间复杂度分析:
    粗略估算,可以认为是在循环中执行向下调整,为 O(n * log(n))
    (了解)实际上是 O(n)

5、插入一个元素

  • 过程(以大堆为例):
  1. 首先按尾插方式放入数组(空间不够时需要扩容)
  2. 比较其和其双亲的值的大小,如果双亲的值大,则满足堆的性质,插入结束
  3. 否则,交换其和双亲位置的值,重新进行 2、3 步骤(2、3就是向上调整的过程)
  4. 直到根结点
  • 图示
    在这里插入图片描述
    是一个向上调整的过程

  • 代码


    /**
     * 向上调整
     * @param child 拿到的数组最后一个位置
     */
    public void adjustUp(int child) {
        while (child > 0) {
            int parent = (child - 1)/ 2;
            if (this.elem[child] <= this.elem[parent]) {
                break;
            }
            int tmp = this.elem[parent];
            this.elem[parent] = this.elem[child];
            this.elem[child] = tmp;
            child = parent;
        }
    }

    /**
     * 添加一个元素(入队列)
     * @param val 要插入的元素
     */
    public void push(int val) {
        if (isFull()) {
            //扩容
            this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
        }

        this.elem[this.usedSize] = val;
        this.usedSize++;
        adjustUp(this.usedSize-1);
    }
    //判断堆是否满了
    public boolean isFull() {
        return this.usedSize == this.elem.length;
    }

6、删除一个元素

为了防止破坏堆的结构,删除时并不是直接将堆顶元素删除,而是

  1. 用数组的最后一个元素替换堆顶元素 ,usedSize–
  2. 然后从堆顶0号位置下标的元素开始,通过向下调整方式重新调整成堆
    在这里插入图片描述

向下调整的代码前面有写过了,可以直接用

    //判断堆是否为空
    public boolean isEmpty() {
        return this.usedSize == 0;
    }

    /**
     * 删除一个元素
     */
    public void pop() {
        if (isEmpty()) {
            throw new RuntimeException("堆为空");
        }
        
        this.elem[0] = this.elem[this.usedSize-1];
        this.usedSize--;
        adjustDown(0,this.usedSize);
    }

7、返回堆顶元素(优先级最高)

    public int getTop() {
        if (isEmpty()) {
            throw new RuntimeException("堆为空");
        }

        return this.elem[0];
    }

三、堆排序


     /**
     * 堆排序
     */
    public void heapSort() {
        int end = this.usedSize - 1;
        while (end > 0) {
            int tmp = this.elem[0];
            this.elem[0] = this.elem[end];
            this.elem[end] = tmp;
            adjustDown(0,end); //该方法在二.3写过了
            end--;
        }
    }

四、topK问题

最小k个数

//不常用
class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];

        if (arr == null || k <= 0) {
            return ret;
        }
        
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        for (int i = 0; i < arr.length; i++) {
            queue.offer(arr[i]);
        }
        
        for (int i = 0; i < k; i++) {
            ret[i] = queue.poll();
        }

        return ret;
    }
}

topK:有一组无序的数据,且数量庞大,求前K个最小的元素或者前K个最大的元素

比如说现在有 N 个元素,求前 K 个最小的元素

  1. 建立大小为 N 的小堆,每次弹出堆顶元素,弹 K 次 (不常用)
  2. 建立大小为 K 的大堆(求前K个最大的元素建小堆)
    1 ) 将待排序序列的前 K 个元素,建成大根堆
    2 ) 遍历剩下的待排序序列,每拿到一个数字,就和当前堆顶元素比较
    3 ) 如果比当前的堆顶元素大,不care
    4 ) 如果比堆顶元素小,那么弹出堆顶元素,将待排序序列当中的数字放到堆中
    第4)中的弹出和放入都对应了一次调整为大根堆的过程
  • 21
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
关于java程序员发展需要学习的路线整理集合 技术 应用技术 计算机基础知识 cpu mem disk net 线程,进程 第三方库 poi Jsoup zxing Gson 数据结构 树 栈 链表 队列 图 操作系统 linux 代码控制 自动化代码检查 sonar 代码规范 阿里巴巴Java开发规范手册 UMPAY——编码规范 日志规范 异常规范 网络 协议 TCP/IP HTTP hession file HTTPS 负载均衡 容器 JBOSS tomcat resin jetty 容灾 日志框架 开源框架 slf4j 框架实现 log4j logback commong logging jdk logger 测试框架 测试框架 junit easymock testng mockito bug管理 禅道 jira 开发工具 编程工具 eclipse myeclipse idea vi VS webstorm sublime text 版本控制 svn git 项目管理 maven Nexus Jenkins 工作软件 反编译软件 office系列 下载器 adobe系列 记录软件 思维导图 office--Note 邮件管理 性能优化 分层优化 系统级别 中间件级别 JVM级别 代码级别 分段优化 前端 web应用 服务应用 资源池 据库 大据与nosql zookeeper hadoop hbase mongodb strom spark java语言 语言语法基础 异常 泛型 内部类 反射 序列化 nIo 匿名类 包装类 优先级 引用 语言工具类库 容器类 集合 链表 map 工具类 系统类 日期类 字类 字符串+正则 流 字符流 字节流 语言特性 继承 封装 多态 JVM 多线程与并发 GC机制 GC收集器类型 串行 CMS 并行 G1 算法 复制 标记清理 标记整理 分区 新生代 eden survivor 老年代(old区) 永久代(perm区) 版本变化 1.5 1.6 1.7 1.8 1.9 IO/NIO IO类型 同步阻塞 同步非阻塞 基于信号 多路复用 异步IO 类加载机制 双亲委派 OSGI 算法 搜索 二分 排序 选择 冒泡 插入 快速 归并 桶 基 常用算法 贪婪 回溯 剪枝 动态规划 据挖掘算法 KMP算法 GZZ算法 HASH分桶 关联规则算法 APRORIVE算法 分布式 负载均衡 水平伸缩 集群 分片 Key-hash 异步 一致性hash 消峰 分库分表 锁 悲观锁 乐观锁 行级锁 分布式锁 分区排队 一致性 一致性算法 paxos zab nwr raft gossip 柔性事务(TCC) 一致性原理 CAP BASE 中间件 据库 mysql 存储引擎 索引 锁 oracle db2 缓存 redis 数据结构 持久 复制 cas 单线程 memcache eacache Tair 消息队列 jms Queue Topic kafka 持久 复制 Stream Partition rocketMQ RabbitMQ ActiveMQ 常用开源框架 Spring Spring MVC Spring WebFlow spring tx aop ioc Struts ibatis Mybatis CAS Dubbo 工作能力 软实力 应急能力 创新能力 管理能力 分享能力 学习能力 沟通能力 解决问题能力 经历 技术攻关案例 程序开发案例 程序设计案例 设计 设计原则 单一职责原则 开闭原则 里氏替换原则 依赖倒转原则 接口隔离原则 迪米特原则 设计模式 结构模式 适配器模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 创建模式 抽象工厂模式 工厂方法模式 建造这模式 原型模式 单例模式 行为模式 责任链模式 命令模式 解释器模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 模板方法模式 访问者模式 设计案例 UML 架构 系统架构能力 基本理论 扩展性设计 可用性设计 可靠性设计 一致性设计 负载均衡设计 过载保护设计 协议设计 二进制协议 文本协议 接入层架构设计 DNS轮询 动静态分离 静态化 反向代理 LVS F5 CDN 逻辑层架构设计 连接池 串行化技术 影子Master架构 批量写入 配置中心 去中心化 通讯机制 同步 RPC RMI 异步 MQ Cron 据层架构设计 缓存优化 DAO&ORM; 双主架构 主从同步 读写分离 性能优化架构能力 代码级别 关联代码优化 cache对其 分支预测 copy on write 内联优化 系统优化 cache 延迟计算 据预读 异步 轮询与通知 内存池 模块化 工程架构能力 开发语言 运维与监控 监控 系统监控 日志监控 流量监控 接口监控 据库监控 业务监控 性能监控 告警 日志 设计模式 数据结构与算法 各种工具

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值