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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 在讲解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]

满足了最小堆的性质。

 

目录
相关文章
|
1月前
|
Java
【Java基础面试三十二】、new String(“abc“) 是去了哪里,仅仅是在堆里面吗?
这篇文章解释了Java中使用`new String("abc")`时,JVM会将字符串直接量"abc"存入常量池,并在堆内存中创建一个新的String对象,该对象会指向常量池中的字符串直接量。
|
23天前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
101 4
|
29天前
|
存储 算法 Java
解释 Java 堆空间和垃圾收集
【8月更文挑战第22天】
25 0
|
2月前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
48 0
|
29天前
|
存储 缓存 Java
|
29天前
|
存储 Java 程序员
Java 中的堆栈和堆有什么区别?
【8月更文挑战第22天】
58 0
|
2月前
|
存储 Java 程序员
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
54 10
|
2月前
|
分布式计算 DataWorks Java
DataWorks操作报错合集之使用ODPS Tunnel Upload功能时,遇到报错:Java 堆内存不足,该如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
2月前
|
存储 安全 Java
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
53 3
|
2月前
|
存储 缓存 监控
Java面试题:解释堆和栈的OutOfMemoryError通常在什么情况下会发生
Java面试题:解释堆和栈的OutOfMemoryError通常在什么情况下会发生
39 3