解析Java中PriorityQueue优先级队列结构的源码及用法

一、PriorityQueue的数据结构

JDK7中PriorityQueue(优先级队列)的数据结构是二叉堆。准确的说是一个最小堆。

二叉堆是一个特殊的堆, 它近似完全二叉树。二叉堆满足特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。
当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。
下图是一个最大堆

priorityQueue队头就是给定顺序的最小元素。

priorityQueue不允许空值且不支持non-comparable的对象。priorityQueue要求使用Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。

priorityQueue的大小是无限制的(unbounded), 但在创建时可以指定初始大小。当增加队列元素时,队列会自动扩容。

priorityQueue不是线程安全的, 类似的PriorityBlockingQueue是线程安全的。

我们知道队列是遵循先进先出(First-In-First-Out)模式的,但有些时候需要在队列中基于优先级处理对象。举个例子,比方说我们有一个每日交易时段生成股票报告的应用程序,需要处理大量数据并且花费很多处理时间。客户向这个应用程序发送请求时,实际上就进入了队列。我们需要首先处理优先客户再处理普通用户。在这种情况下,Java的PriorityQueue(优先队列)会很有帮助。

PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。
优先队列不允许空值,而且不支持non-comparable(不可比较)的对象,比如用户自定义的类。优先队列要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。

优先队列的头是基于自然排序或者Comparator排序的最小元素。如果有多个对象拥有同样的排序,那么就可能随机地取其中任意一个。当我们获取队列时,返回队列的头对象。

优先队列的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加。

PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境。

二、PriorityQueue源码分析

成员:

priavte transient Object[] queue;
private int size = 0;

1.PriorityQueue构造小顶堆的过程

这里我们以priorityQueue构造器传入一个容器为参数PriorityQueue(Collecntion<? extends E>的例子:

构造小顶堆的过程大体分两步:

复制容器数据,检查容器数据是否为null

private void initElementsFromCollection(Collection<? extends E> c) {
  Object[] a = c.toArray();
  // If c.toArray incorrectly doesn't return Object[], copy it.
  if (a.getClass() != Object[].class)
    a = Arrays.copyOf(a, a.length, Object[].class);
  int len = a.length;
  if (len == 1 || this.comparator != null)
    for (int i = 0; i < len; i++)
      if (a[i] == null)
        throw new NullPointerException();
  this.queue = a;
  this.size = a.length;
}

调整,使数据满足小顶堆的结构。
首先介绍两个调整方式siftUp和siftDown

siftDown: 在给定初始化元素的时候,要调整元素,使其满足最小堆的结构性质。因此不停地从上到下将元素x的键值与孩子比较并做交换,直到找到元素x的键值小于等于孩子的键值(即保证它比其左右结点值小),或者是下降到叶子节点为止。
例如如下的示意图,调整9这个节点:

private void siftDownComparable(int k, E x) {
  Comparable<? super E> key = (Comparable<? super E>)x;
  int half = size >>> 1;    // size/2是第一个叶子结点的下标
  //只要没到叶子节点
  while (k < half) {
    int child = (k << 1) + 1; // 左孩子
    Object c = queue[child];
    int right = child + 1;
    //找出左右孩子中小的那个
    if (right < size &&
      ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
      c = queue[child = right];
    if (key.compareTo((E) c) <= 0)
      break;
    queue[k] = c;
    k = child;
  }
  queue[k] = key;
}

siftUp: priorityQueue每次新增加一个元素的时候是将新元素插入对尾的。因此,应该与siftDown有同样的调整过程,只不过是从下(叶子)往上调整。
例如如下的示意图,填加key为3的节点:

private void siftUpComparable(int k, E x) {
  Comparable<? super E> key = (Comparable<? super E>) x;
  while (k > 0) {
    int parent = (k - 1) >>> 1;   //获取parent下标
    Object e = queue[parent];
    if (key.compareTo((E) e) >= 0)
      break;
    queue[k] = e;
    k = parent;
  }
  queue[k] = key;
}

总体的建立小顶堆的过程就是:

private void initFromCollection(Collection<? extends E> c) {
    initElementsFromCollection(c);
    heapify();
  }

其中heapify就是siftDown的过程。

2.PriorityQueue容量扩容过程

从实例成员可以看出,PriorityQueue维护了一个Object[], 因此它的扩容方式跟顺序表ArrayList相差不多。
这里只给出grow方法的源码

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                     (oldCapacity + 2) :
                     (oldCapacity >> 1));
    // overflow-conscious code
    if (newCapacity - MAX_ARRAY_SIZE > 0)
      newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
  }

可以看出,当数组的Capacity不大的时候,每次扩容也不大。当数组容量大于64的时候,每次扩容double。

三、PriorityQueue的应用

eg1:
这里给出一个最简单的应用:从动态数据中求第K个大的数。
思路就是维持一个size = k 的小顶堆。

  //data是动态数据
  //heap维持动态数据的堆
  //res用来保存第K大的值
  public boolean kthLargest(int data, int k, PriorityQueue<Integer> heap, int[] res) {
    if(heap.size() < k) {
      heap.offer(data);
      if(heap.size() == k) {
        res[0] = heap.peek();
        return true;
      }
      return false;
    }
    if(heap.peek() < data) {
      heap.poll();
      heap.offer(data);
    }
    res[0] = heap.peek();
    return true;
  }

eg2:
我们有一个用户类Customer,它没有提供任何类型的排序。当我们用它建立优先队列时,应该为其提供一个比较器对象。

Customer.java

package com.journaldev.collections;

public class Customer {

  private int id;
  private String name;

  public Customer(int i, String n){
    this.id=i;
    this.name=n;
  }

  public int getId() {
    return id;
  }

  public String getName() {
    return name;
  }

}

我们使用Java随机数生成随机用户对象。对于自然排序,我们使用Integer对象,这也是一个封装过的Java对象。
下面是最终的测试代码,展示如何使用PriorityQueue:

PriorityQueueExample.java

package com.journaldev.collections;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;

public class PriorityQueueExample {

  public static void main(String[] args) {

    //优先队列自然排序示例
    Queue<Integer> integerPriorityQueue = new PriorityQueue<>(7);
    Random rand = new Random();
    for(int i=0;i<7;i++){
      integerPriorityQueue.add(new Integer(rand.nextInt(100)));
    }
    for(int i=0;i<7;i++){
      Integer in = integerPriorityQueue.poll();
      System.out.println("Processing Integer:"+in);
    }

    //优先队列使用示例
    Queue<Customer> customerPriorityQueue = new PriorityQueue<>(7, idComparator);
    addDataToQueue(customerPriorityQueue);

    pollDataFromQueue(customerPriorityQueue);

  }

  //匿名Comparator实现
  public static Comparator<Customer> idComparator = new Comparator<Customer>(){

    @Override
    public int compare(Customer c1, Customer c2) {
      return (int) (c1.getId() - c2.getId());
    }
  };

  //用于往队列增加数据的通用方法
  private static void addDataToQueue(Queue<Customer> customerPriorityQueue) {
    Random rand = new Random();
    for(int i=0; i<7; i++){
      int id = rand.nextInt(100);
      customerPriorityQueue.add(new Customer(id, "Pankaj "+id));
    }
  }

  //用于从队列取数据的通用方法
  private static void pollDataFromQueue(Queue<Customer> customerPriorityQueue) {
    while(true){
      Customer cust = customerPriorityQueue.poll();
      if(cust == null) break;
      System.out.println("Processing Customer with ID="+cust.getId());
    }
  }

}

注意我用实现了Comparator接口的Java匿名类,并且实现了基于id的比较器。
当我运行以上测试程序时,我得到以下输出:

Processing Integer:9
Processing Integer:16
Processing Integer:18
Processing Integer:25
Processing Integer:33
Processing Integer:75
Processing Integer:77
Processing Customer with ID=6
Processing Customer with ID=20
Processing Customer with ID=24
Processing Customer with ID=28
Processing Customer with ID=29
Processing Customer with ID=82
Processing Customer with ID=96

从输出结果可以清楚的看到,最小的元素在队列的头部因而最先被取出。如果不实现Comparator,在建立customerPriorityQueue时会抛出ClassCastException。

Exception in thread "main" java.lang.ClassCastException: com.journaldev.collections.Customer cannot be cast to java.lang.Comparable
  at java.util.PriorityQueue.siftUpComparable(PriorityQueue.java:633)
  at java.util.PriorityQueue.siftUp(PriorityQueue.java:629)
  at java.util.PriorityQueue.offer(PriorityQueue.java:329)
  at java.util.PriorityQueue.add(PriorityQueue.java:306)
  at com.journaldev.collections.PriorityQueueExample.addDataToQueue(PriorityQueueExample.java:45)
  at com.journaldev.collections.PriorityQueueExample.main(PriorityQueueExample.java:25)
(0)

相关推荐

  • JDK源码之PriorityQueue解析

    一.优先队列的应用 优先队列在程序开发中屡见不鲜,比如操作系统在进行进程调度时一种可行的算法是使用优先队列,当一个新的进程被fork()出来后,首先将它放到队列的最后,而操作系统内部的Scheduler负责不断地从这个优先队列中取出优先级较高的进程执行:爬虫系统在执行时往往也需要从一个优先级队列中循环取出高优先级任务并进行抓取.可以想见,如果类似这样的任务不适用优先级进行划分的话,系统必会出现故障,例如操作系统中低优先级进程持续占用资源而高优先级进程始终在队列中等待.此外,优先队列在贪婪算法中也

  • 解决java 查看JDK中底层源码的实现方法

    1.点 "window"-> "Preferences" -> "Java" -> "Installed JRES"2.此时"Installed JRES"右边是列表窗格,列出了系统中的 JRE 环境,选择你的JRE,然后点边上的 "Edit...", 会出现一个窗口(Edit JRE)3.选中rt.jar文件的这一项:"c:\program files\ja

  • 解析Java中PriorityQueue优先级队列结构的源码及用法

    一.PriorityQueue的数据结构 JDK7中PriorityQueue(优先级队列)的数据结构是二叉堆.准确的说是一个最小堆. 二叉堆是一个特殊的堆, 它近似完全二叉树.二叉堆满足特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆. 当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆. 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆. 下图是一个最大堆 priorityQueue队头就是给定顺序的最小元素. prio

  • Java中PriorityQueue实现最小堆和最大堆的用法

    一.基本介绍 1.介绍 学习很多算法知识,力争做到最优解的学习过程中,很多时候都会遇到PriorityQueue(优先队列).一个基于优先级堆的无界优先级队列.优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法.优先级队列不允许使用 null 元素.依靠自然顺序的优先级队列还不允许插入不可比较的对象,这样做可能导致 ClassCastException. 此队列的头是按指定排序方式确定的最小元素.如果多个元素都是最小值,则

  • java中栈和队列的实现和API的用法(详解)

    在java中要实现栈和队列,需要用到java集合的相关知识,特别是Stack.LinkedList等相关集合类型. 一.栈的实现 栈的实现,有两个方法:一个是用java本身的集合类型Stack类型:另一个是借用LinkedList来间接实现Stack. 1.Stack实现 直接用Stack来实现非常方便,常用的api函数如下: boolean        isEmpty() // 判断当前栈是否为空 synchronized E        peek() //获得当前栈顶元素 synchro

  • Java中String.split()的最详细源码解读及注意事项

    目录 前言 一.split(regex,limit) 二.split(regex) 总结 前言 博主针对字符串分割时出现的各种空字符串问题,进入String类的源码看了一下,现作如下解读及演示: 一.split(regex,limit) 首先是带有两个参数的split方法: 作用: 将以给定正则表达式(regex)的字符串分隔开来 第一个参数是传入字符类型的分隔符,如 “,” 等(可以是任何字符串) 第二个参数传入整型的limit,代表的是将此字符串分割成n部分(这里的n就是limit). 返回

  • C#中实现PriorityQueue优先级队列的代码

    前言 前段时间看到有大佬对.net 6.0新出的PriorityQueue(优先级队列)数据结构做了解析,但是没有源码分析,所以本着探究源码的心态,看了看并分享出来.它不像普通队列先进先出(FIFO),而是根据优先级出队. ps:读者多注意代码的注释. D叉树的认识(d-ary heap) 首先我们在表示一个堆(大顶堆或小顶堆)的时候,实际上是通过一个一维数组来维护一个二叉树(d=2,d表示每个父节点最多有几个子节点),首先看下图的二叉树,数字代表索引: 任意一个节点的父节点的索引为:(inde

  • Java数组模拟优先级队列数据结构的实例

    优先级队列 如果我们给每个元素都分配一个数字来标记其优先级,不妨设较小的数字具有较高的优先级,这样我们就可以在一个集合中访问优先级最高的元素并对其进行查找和删除操作了.这样,我们就引入了优先级队列 这种数据结构. 优先级队列(priority queue) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有(1)查找(2)插入一个新元素 (3)删除 一般情况下,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素 .对于优先权相同的元素,可按先进先出次序处理或按任意优先

  • Java数据结构之优先级队列(PriorityQueue)用法详解

    目录 概念 PriorityQueue的使用 小试牛刀(最小k个数) 堆的介绍 优先级队列的模拟实现 Top-k问题 概念 优先级队列是一种先进先出(FIFO)的数据结构,与队列不同的是,操作的数据带有优先级,通俗的讲就是可以比较大小,在出队列的时候往往需要优先级最高或者最低的元素先出队列,这种数据结构就是优先级队列(PriorityQueue) PriorityQueue的使用 构造方法 这里只介绍三种常用的构造方法 构造方法 说明 PriorityQueue() 不带参数,默认容量为11 P

  • 解析java中的condition

    一.condition 介绍及demo Condition是在java 1.5中才出现的,它用来替代传统的Object的wait().notify()实现线程间的协作,相比使用Object的wait().notify(),使用Condition的await().signal()这种方式实现线程间协作更加安全和高效.因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作. Condition是个接口,基本的方法就是await()和signal()方法:

  • Java中的阻塞队列详细介绍

    Java中的阻塞队列 1. 什么是阻塞队列? 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列.这两个附加的操作是: 在队列为空时,获取元素的线程会等待队列变为非空. 当队列满时,存储元素的线程会等待队列可用. 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程.阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素. 2.Java里的阻塞队列 JDK中提供了七个阻塞队列: ArrayBlockingQueue :一个由数组结

  • 详解java中的阻塞队列

    阻塞队列简介 阻塞队列(BlockingQueue)首先是一个支持先进先出的队列,与普通的队列完全相同: 其次是一个支持阻塞操作的队列,即: 当队列满时,会阻塞执行插入操作的线程,直到队列不满. 当队列为空时,会阻塞执行获取操作的线程,直到队列不为空. 阻塞队列用在多线程的场景下,因此阻塞队列使用了锁机制来保证同步,这里使用的可重入锁: 而对于阻塞与唤醒机制则有与锁绑定的Condition实现 应用场景:生产者消费者模式 java中的阻塞队列 java中的阻塞队列根据容量可以分为有界队列和无界队

随机推荐