《徐徐道来话Java》:PriorityQueue和最小堆

简介: 在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆。 最小堆是一种经过排序的完全二叉树,其中任一非终端节点数值均不大于其左孩子和右孩子节点的值。 可以得出结论,如果一棵二叉树满足最小堆的要求,那么,堆顶(根节点)也就是整个序列的最小元素。

在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆

最小堆是一种经过排序的完全二叉树,其中任一非终端节点数值均不大于其左孩子和右孩子节点的值。

可以得出结论,如果一棵二叉树满足最小堆的要求,那么,堆顶(根节点)也就是整个序列的最小元素。

最小堆的例子如下图所示:

 

 

可以注意到,20的两个子节点3121,和它们的叔节点30并没有严格的大小要求。以广度优先的方式从根节点开始遍历,可以构成序列:

[10,20,30,31,21,32,70]

反过来,可以推演出,序列构成二叉树的公式是:对于序列中下标为i的元素,左孩子为left(i) = i*2 +1,右孩子为 right(i) = left(i)+1

现在可以思考一个问题,对于给定的序列,如何让它满足最小堆的性质?

例如[20, 10, 12, 1, 7, 32, 9],可以构成二叉树:

 

 

这里提供一个方法:

 

1、倒序遍历数列;

 

2、挨个进行沉降处理,沉降过程为:和左右子节点中的最小值比对,如果比最小值要大,则和该子节点交换数据,反之则不做处理,继续1过程;

 

3、沉降后的节点,再次沉降,直到叶子节点。

 

同时,因为下标在size/2之后的节点是叶子节点,无需比对,所以可以从size/2-1位置开始倒序遍历,节约执行次数。

 

应用该方法对之前的数列进行解析:

 

1、数列[20,10,12,1,7,32,9]长度为7,所以size/2 - 1 =2,倒序遍历过程是12 -> 10 ->20

 

2、12的左孩子为32,右孩子为912>9,进行沉降,结果如下图所示:

 

 

 

 

310的左孩子为1,右孩子为710 > 1,进行沉降,结果如下图所示:

 

 

 

 

 
   

420的左孩子为1,右孩子为920 > 1,进行沉降,结果如下图所示:

 

 

520的左孩子为10,右孩子为720 > 7,进行沉降,得到最终结果:

 

 

 

 

 
   

满足最小堆的要求,此时,得出的序列为[1,7,9,10,20,32,12]

该实现的流程也就是PriorityQueueheapify方法的流程,heapify方法负责把序列转化为最小堆,也就是所谓的建堆。其源码如下所示:

 

 

private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

 

siftDown方法也就是之前提过的沉降

 

 siftDown(k,x)方法解析

 

 

siftDown这个方法,根据comparator成员变量是否为null,它的执行方式略有不同:

 

如果comparator不为null,调用comparator进行比较;

 

反之,则把元素视为Comparable进行比较;

 

如果元素不为Comparable的实现,则会抛出ClassCastException

 

不论哪种,执行的算法是一样的,这里只做Comparator的源码解析:

 

private void siftDownUsingComparator(int k, E x) {
       //只查找非叶子节点
    int half = size >>> 1;
    while (k < half) {
              //左孩子
        int child = (k << 1) + 1;
        Object c = queue[child];
              //右孩子
        int right = child + 1;
              //取左右孩子中的最小者
        if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
              //父节点比最小孩子小说明满足最小堆,结束循环
        if (comparator.compare(x, (E) c) <= 0)
            break;
              //交换父节点和最小孩子位置,继续沉降
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

 

注释已经解释清楚了代码的执行逻辑,其目的是把不满足最小堆条件的父节点一路沉到最底部。从以上代码可以看出,siftDown的时间复杂度不会超出O(logn)

 

siftUp(k,x)方法解析

siftUp方法用于提升节点。新加入的节点一定在数列末位,为了让数列满足最小堆性质,需要对该节点进行提升操作。

siftDown一样,它也有两种等效的实现路径,这里只做shifUpUsingComparator的解析:

 

  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;
    }

 

节点的插入,是在数列的尾端的,它很可能比父节点要小,不满足最小堆的定义,所以,需要做上浮的操作。

这里提供一个例子帮助理解,有最小堆数列[10203040305070],构成最小堆如下所示:

 

 

 

 

 

 

1、执行添加19,变为:

 

 
   
 
   

219<40,与40交换位置:

 

319<20,与20交换位置:

419>10,终止上浮操作,最后得到的数列为:

[1019302030507040]

满足了最小堆的性质。

 

目录
相关文章
【Java基础面试三十二】、new String(“abc“) 是去了哪里,仅仅是在堆里面吗?
这篇文章解释了Java中使用`new String("abc")`时,JVM会将字符串直接量"abc"存入常量池,并在堆内存中创建一个新的String对象,该对象会指向常量池中的字符串直接量。
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
612 4
|
存储 算法 Java
解释 Java 堆空间和垃圾收集
【8月更文挑战第22天】
209 0
|
监控 Java 中间件
8G的容器Java堆才4G怎么就OOM了?
本文记录最近一例Java应用OOM问题的排查过程,希望可以给遇到类似问题的同学提供参考。
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
425 4
Java对象一定分配在堆上吗?
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
487 5
|
存储 Java 调度
Java 中的优先队列 PriorityQueue 详解
【10月更文挑战第22天】优先队列 PriorityQueue 是一种非常实用的数据结构,在许多场景中都能发挥重要作用。通过深入了解其特点和用法,我们可以更好地利用它来解决实际问题。
871 13
|
存储 算法 Java
🏗️Java零基础:深入了解Java 堆
【10月更文挑战第2天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
298 3
|
JSON 前端开发 JavaScript
java中post请求调用下载文件接口浏览器未弹窗而是返回一堆json,为啥
客户端调接口需要返回另存为弹窗,下载文件,但是遇到的问题是接口调用成功且不报错,浏览器F12查看居然返回一堆json,而没有另存为弹窗; > 正确的效果应该是:接口调用成功且浏览器F12不返回任何json,而是弹窗另存为窗口,直接保存文件即可。
654 2
|
存储 Java 程序员
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
666 10