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

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

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

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

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

可以注意到,20的两个子节点31、21,和它们的叔节点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,右孩子为9,12>9,进行沉降,结果如下图所示:

3、10的左孩子为1,右孩子为7,10 > 1,进行沉降,结果如下图所示:

 
   

4、20的左孩子为1,右孩子为9,20 > 1,进行沉降,结果如下图所示:

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

 
   

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

该实现的流程也就是PriorityQueue的heapify方法的流程,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;
    }

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

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

1、执行添加19,变为:

 
   
 
   

2、19<40,与40交换位置:

3、19<20,与20交换位置:

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

[10,19,30,20,30,50,70,40]

满足了最小堆的性质。

时间: 2024-10-07 01:04:57

《徐徐道来话Java》:PriorityQueue和最小堆的相关文章

《徐徐道来话Java》:泛型的基本概念(1)

泛型是一种编程范式(Programming Paradigm),是为了效率和重用性产生的.由Alexander Stepanov(C++标准库主要设计师)和David Musser(伦斯勒理工学院CS名誉教授)首次提出,自实现始,就成为了ANSI/ISO C++重要标准之一. Java自1.5版本开始提供泛型,其本质是一个参数化的类型,那么,何谓参数化? 参数是一个外部变量.设想一个方法,其参数的名称和实际的数值是外部传入的,那么,该参数的类型是否也作为一个参数,在运行时决定呢?这就是泛型的作用

深入理解Java PriorityQueue

深入理解Java PriorityQueue PriorityQueue 本文github地址 Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示.本文从Queue接口函数出发,结合生动的图解,深入浅出地分析PriorityQueue每个操作的具体过程和时间复杂度,将让读者建立对PriorityQueue建立清晰而深入的认识. 总体介绍 前面以Java ArrayDeque为例讲解了Stack和Queue,其实还有一种特殊的队列叫做PriorityQueue,即优先

Google 面试题:Java实现用最大堆和最小堆查找中位数 Find median with min heap and max heap in Java

Google面试题 股市上一个股票的价格从开市开始是不停的变化的,需要开发一个系统,给定一个股票,它能实时显示从开市到当前时间的这个股票的价格的中位数(中值). SOLUTION 1: 1.维持两个heap,一个是最小堆,一个是最大堆. 2.一直使maxHeap的size大于minHeap. 3. 当两边size相同时,比较新插入的value,如果它大于minHeap的最大值,把它插入到minHeap.并且把minHeap的最小值移动到maxHeap. ...具体看代码 1 /*********

100多道经典的JAVA面试题及答案解析

面向对象编程(OOP) Java是一个支持并发.基于类和面向对象的计算机编程语言.下面列出了面向对象软件开发的优点: 代码开发模块化,更易维护和修改. 代码复用. 增强代码的可靠性和灵活性. 增加代码的可理解性. 面向对象编程有很多重要的特性,比如:封装,继承,多态和抽象.下面的章节我们会逐个分析这些特性. 封装 封装给对象提供了隐藏内部特性和行为的能力.对象提供一些能被其他对象访问的方法来改变它内部的数据.在Java当中,有3种修饰符:public,private和protected.每一种修

50道经典的JAVA编程题(目录)

50道经典的JAVA编程题(目录) 题目来源于:http://blog.sina.com.cn/s/blog_60fafdda0100wb21.html [程序1] TestRabbit.java 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程序分析: 兔子的规律为数列1,1,2,3,5,8,13,21.... [程序2] FindPrimeNumber.java 题目:判断101-20

Top K以及java priorityqueue

Top K问题比较常见啦,这里总结一下方法. 1.用最小堆来做. 思路是先利用数组中前k个数字建一个最小堆,然后将剩余元素与堆顶元素进行比较,如果某个元素比堆顶元素大,就替换掉堆顶元素,并且重新调整成最小堆. 到这里,堆中保存着的其实是前k个最大的数字.堆顶就是第K个最大的数字.这样前k个,第k个都可以求出来了.代码如下: 1 public void find(int[] nums, int k){ 2 PriorityQueue<Integer> priorityQueue = new Pr

Java - PriorityQueue

JDK 10.0.2 前段时间在网上刷题,碰到一个求中位数的题,看到有网友使用PriorityQueue来实现,感觉其解题思想挺不错的.加上我之前也没使用过PriorityQueue,所以我也试着去读该类源码,并用同样的思想解决了那个题目.现在来对该类做个总结,需要注意,文章内容以算法和数据结构为中心,不考虑其他细节内容.如果小伙伴想看那个题目,可以直接跳转到(小测试). 目录 一. 数据结构:queue[].size.comparator 二. 初始化(堆化):heapify().siftDo

【转载】20道常见初级Java面试题

这篇文章的内容很不错.学到了很多东西.值得仔细琢磨. http://mt.sohu.com/20160831/n466900239.shtml 20道常见初级Java面试题,入职者必备! 广州华信智原2016-08-31 09:30:37阅读(564)评论(0) 声明:本文由入驻搜狐公众平台的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场.举报 大家都应该知道Java是目前最火的计算机语言之一,连续几年蝉联最受程序员欢迎的计算机语言榜首,因此每年新入职Java程序员也数不胜数.究

java实现——030最小的k个数

1.O(nlogk)海量数据 1 import java.util.TreeSet; 2 3 public class T030 { 4 public static void main(String[] args){ 5 int[] data = {4,5,1,6,2,7,3,8}; 6 TreeSet<Integer> leastNumbers = new TreeSet<Integer>(); 7 getLeastNumbers(data,leastNumbers,4); 8