史上最强优先级队列教程PriorityQueue、DelayedWorkQueue

写这篇文章的目的是,我们寝室大哥,龙哥正在培训java,为了让龙哥更好的理解堆这个数据结构的内容,模拟他以后面试经历的场景。

面试经过

面试官: 你好,欢迎再次参加面试,几天不见,感觉头发又少了呢?

龙哥: 本来头发就少,又当了程序员,哎。

在这里插入图片描述

面试官: 我们回归正题,上回说到ScheduledThreadPoolExecutor,那ScheduledThreadPoolExecutor里面用的什么阻塞队列?

龙哥: 这个说起来就有点复杂了,首先我们通过ScheduledThreadPoolExecutor构造方法来看,都是调用了父类ThreadPoolExecutor的构造函数,然后阻塞队列使用的都是DelayedWorkQueue。

在这里插入图片描述
DelayedWorkQueue这个队列的最大容量是Integer.MAX_VALUE,近似是一种无界队列,毕竟一般也不会有这么多的定时任务不是?而且只能接受RunnableScheduledFuture类型的任务,是一种特殊定制化的阻塞队列。

面试官: 那么为什么周期性线程池ScheduledThreadPoolExecutor要选择这种阻塞队列来存放待执行的任务啊,它和平常我们用的哪种队列比较像呢?

龙哥: 它其实实现方式和我们常见的优先级队列PriorityQueue是差不多的,但是它是线程安全的。至于为什么要选择一种类似于优先级队列的阻塞队列,那是因为周期性线程池ScheduledThreadPoolExecutor的特性决定的,线程池里面的任务都是由时间的,根据要执行时间的远近可以区分优先级是怎么样的,换句话说,我们可以把最快要执行的任务放到最前面,这样的话,我线程池就只用监视第一个任务,如果它都没有到时间的话,所有的任务就都没有到时间,这样比遍历所有的任务来看谁到时间了,要快的多。

面试官: (这小子怎么什么都知道,不行我再往下问问)那我就很好奇了,照你说DelayedWorkQueue、PriorityQueue这两种队列还是慢厉害的嘛,那他们是怎么做到永远都能能到最优先的任务的呢?是不是遍历一遍所有的任务,把最优先的放到最前面啊?

龙哥: (又给我挖坑)那肯定不是的,如果是每次都要遍历一遍的话,那么就需要O(n)的时间复杂度,效率就很低。所以其实DelayedWorkQueue、PriorityQueue这两种队列底层是用堆这种数据结构存储的。

面试官: 哦?堆,那你能详细的说一下堆是个怎么回事么?

龙哥: (对于能手写十种排序算法的我,堆还不是信手拈来),堆其实就是就是一个数组,但是这个数组我们是可以把它想成一个完全二叉树的,就比如有数组【4、6、8、5、9】,我们就可以把它看成完全二叉树。

在这里插入图片描述
堆就是一个完全二叉树,但是堆又分为大顶堆和小顶堆。

大顶堆:每个结点的值都大于或等于其左右孩子结点的值(在堆排序算法中用于从小到大排序)。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值(在堆排序算法中用于从大到小排序)。

根据这个来想象,我们的优先级队列,就是每次把最快要到期执行的任务放到堆顶,执行完了之后,再重新构建一个大顶堆,这样就能每次拿到最快要执行的任务。

面试官: 那你能拿你举的数组的例子和我详细说一下,构建大顶堆的过程么?

龙哥: 这个还是画个图吧,你有笔嘛?

在这里插入图片描述
这里面涉及到几个堆的比较重要的公式:

如何确认第一个非叶子节点: arr.length / 2 - 1
如何确认一个节点的左子节点的位置:i * 2 + 1 (i是父节点在数组中的索引值)
如何确认一个节点的父节点的位置:(i - 1) / 2 (i是子节点在数组中的索引值)

面试官: 不错,写出这个堆排序应该就没有问题,但是对于优先级队列来讲,向队列里面添加元素,是怎么形成的顶堆呢?

龙哥: 这个嘛,其实原理差不错,优先级队列底层维护了一个数组,如果数组为空的话,就给索引0赋值,如果要查看堆顶的元素的话,也直接返回索引值为0的元素;如果数组不为空的话,那么就把这个元素放到当前容量加一的位置,假设是索引值 i 的位置,(如果容量不够了就要扩容)。然后就要对这个新元素进行上浮,也就是执行 siftUp 方法。
在这里插入图片描述
这个方法的逻辑也比较简单,就是找到当前位置的元素的父节点,如果父节点没有我新加的这个元素这么着急的话,大家就换一个位置。直到父节点比我还急,或者上升到了数组的首位(索引值为0),下一个就轮到我执行了,这就是上浮的过程。至于在完全二叉树中如何确认父节点的位置,上面的公式有写 (i - 1) / 2 (i是子节点在数组中的索引值)。额,还是不明白,好吧,我再画个图。

在这里插入图片描述
我们可以看到,每次上浮最大也是是树的高度,时间复杂度也就是O(lgn),咳咳,比你上面说的遍历O(n)可强的不是一点半点啊。

面试官: 这小子这么强的嘛,上厕所不带纸啊(怎么讲????? — 高手),那poll元素和删除元素呢。

龙哥: 这个就差不多了,poll之后,就是堆顶的元素拿走了嘛,我们就把堆尾的元素拿到堆顶,再下潜就好 siftDown ,下潜的源码。

在这里插入图片描述

什么什么,还是不明白下潜的过程,算了算了,我再画个图吧,还是用那个数组举例。

在这里插入图片描述
有一个比较难理解的点要注意的是,当删除指定元素的时候,需要先下潜再上浮。

在这里插入图片描述
为什么这么做呢,因为总末尾拿的元素,优先级肯定是比较低的,首先肯定是下潜,但是如果下潜不下去的话,说明移除肯定是叶子节点,那么为什么还要上浮呢?因为可能是在完全二叉树的两个分支上面,你在右面比较优先级低,没准在左边就比较高,还能升上去。

比如删除了元素1,8到元素1的位置了,8和父节点5比,优先级就比较,所以需要上浮的过程。

在这里插入图片描述

面试官: 可以,回去等通知吧,我们改日再战,,,再面试。

龙哥: 别啊,来几道算法题吧。

面试官: 滚!

龙哥: 好嘞。


DelayedWorkQueue实现方式基本和PriorityQueue差不多,就是线程安全的,然后只能处理RunnableScheduledFuture类型的任务,本文就以PriorityQueue举例了。

堆排序

既然都说了堆的详细内容,那么堆排序,大家也应该明白了。下面有堆排序的完整代码,如果有不理解的,欢迎留言。

class Solution {
    public int[] sortArray(int[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }
        int length = arr.length;
        // 初始化构建大顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            adjustHeap(i, arr, length);
        }

        for (int i = length - 1; i > 0; i--) {
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            adjustHeap(0, arr, i);
        }
        return arr;
    }

    private void adjustHeap(int point, int[] arr, int length) {
        int value = arr[point];
        for (int i = 2 * point + 1; i < length; i = 2 * i + 1) {
            if (i + 1 < length && arr[i + 1] > arr[i]) {
                i = i + 1;
            }
            if (arr[i] > value) {
                arr[point] = arr[i];
                point = i;
            } else {
                break;
            }
        }
        arr[point] = value;
    }
}
  • 12
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员大航子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值