服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|JAVA教程|ASP教程|编程技术|正则表达式|

服务器之家 - 编程语言 - JAVA教程 - JDK源码之PriorityQueue解析

JDK源码之PriorityQueue解析

2020-09-18 14:19_fred JAVA教程

这篇文章主要为大家详细介绍了JDK源码之PriorityQueue,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

一.优先队列的应用

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

二.优先队列的实现原理

优先队列的实现方式是使用二叉堆的结构,需要满足以下两条性质(Heap property),这里以小顶堆为例讲解:

  1.任何结点的值都小于或等于其子节点的值。
  2.所有结点从上到下,从左到右填入,即一棵完全二叉树。

基于这两条规律,二叉堆在实现中往往会使用一个数组,下面我们研究一下JDK中二叉堆(优先队列)的实现。

三.优先队列在JDK中的实现方式

研究源码最好的方式是debug,看每一步变量的变化,我们可以简单写一个Demo,debug进源码一探究竟:

JDK源码之PriorityQueue解析

这里我们简单地创建一个优先队列,向其中添加三个元素,我们可以在代码第一行打一个断点,如果您使用Eclipse编辑器的话,接下来可以按F5进入源码中:

JDK源码之PriorityQueue解析

代码运行到这里,PriorityQueue调用自己的一个重载构造器,第一个参数是数组默认大小,第二个是元素比较的Comparator,我们这里的Demo比较简单,您在使用优先队列时可以选择实现自己的Comparator。

?
1
2
3
4
5
6
7
8
9
public PriorityQueue(int initialCapacity,
            Comparator<? super E> comparator) {
   // Note: This restriction of at least one is not actually needed,
   // but continues for 1.5 compatibility
   if (initialCapacity < 1)
     throw new IllegalArgumentException();
   this.queue = new Object[initialCapacity];
   this.comparator = comparator;
 }

接下来我们研究一下添加元素时的offer操作:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean offer(E e) {
   if (e == null)
     throw new NullPointerException();
   //记录了队列被修改的次数
   modCount++;
   int i = size;
   if (i >= queue.length)
     //扩容
     grow(i + 1);
   //增加元素个数
   size = i + 1;
   if (i == 0)
     //第一次添加元素,直接放到第0个位置即可
     queue[0] = e;
   else
     //需要将元素放到最后,再做上滤操作
     siftUp(i, e);
   return true;
 }

我们逐行来解释一下,首先offer方法判断参数是否为空,不为空则对变量modCount自增,modCount记录了队列被修改的次数,接下来,判断数组是否会越界,如果越界则通过grow进行扩容,接下来添加元素,如果当前元素为0个则直接将元素放到数组第一个位置,否则做一个siftUp的操作。

?
1
2
3
4
5
6
7
8
9
10
11
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);

上面的代码对队列扩容,源码中注释也很清晰,首先判断当前的数组大小是否足够小(<64),如果足够小则将大小扩充为2倍,否则将原大小加上50%。需要注意的是,这里最后做了一个大小是否溢出的判断。

?
1
2
3
4
5
6
7
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
      throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
      Integer.MAX_VALUE :
      MAX_ARRAY_SIZE;
  }

如果需要扩容的大小已经<0了,显然已经溢出了,在这里抛出了OutOfMemory的异常。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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;
  }

为了保证优先队列的性质1,在插入每个元素时都需要与该节点父亲进行比较,找到其正确位置,有些数据结构书中,这个操作被称为上滤(percolate up)。

入队操作已经说完了,接下来是出队操作,即poll()操作:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public E poll() {
    if (size == 0)
      return null;
    int s = --size;
    //自增变量,代表队列修改次数
    modCount++;
    E result = (E) queue[0];
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
      siftDown(0, x);
    return result;
  }

这个方法首先将数组第一个元素作为结果,(因为如果是小顶堆的话堆顶始终是最小元素),并将队列的最后一个元素放到第一个位置,最后用siftDown做一些调整,保证队列的性质,这个操作被称为下滤(percolate down)。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void siftDownUsingComparator(int k, E x) {
 
 
   int half = size >>> 1;
   //这里k必须有孩子,故叶节点需要比较
   while (k < half) {
     //以下几行代码到较小的那个儿子,用变量c表示
     int child = (k << 1) + 1;
     //这里假设左儿子比较小
     Object c = queue[child];
     int right = child + 1;
     //左右儿子比较,如果右儿子小则将c赋值为右儿子
     if (right < size &&
       comparator.compare((E) c, (E) queue[right]) > 0)
       c = queue[child = right];
     //如果x比小儿子还小,说明k就是正确位置
     if (comparator.compare(x, (E) c) <= 0)
       break;
     queue[k] = c;
     k = child;
   }
   queue[k] = x;
 }

如上图,下滤过程中k不断与其儿子进行比较,如果满足优先队列的顺序性,则break出循环。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:http://www.cnblogs.com/showing/p/6759341.html

延伸 · 阅读

精彩推荐
  • JAVA教程java中lambda表达式简单用例

    java中lambda表达式简单用例

    让我们从最简单的例子开始,来学习如何对一个string列表进行排序。我们首先使用Java 8之前的方法来实现 ...

    robin4722020-06-15
  • JAVA教程深入理解Java的接口与抽象类

    深入理解Java的接口与抽象类

    本文主要介绍java 的接口和抽象类,对接口和抽象类进行介绍对比,深入理解,有需要的小伙伴可以参考下 ...

    java技术网3142020-05-29
  • JAVA教程Spring Boot中Redis数据库的使用实例

    Spring Boot中Redis数据库的使用实例

    Spring Boot中除了对常用的关系型数据库提供了优秀的自动化支持之外,对于很多NoSQL数据库一样提供了自动化配置的支持。本篇文章主要介绍了Spring Boot中...

    心碎落地的声音1932020-09-06
  • JAVA教程Datagram Scoket双向通信

    Datagram Scoket双向通信

    这篇文章主要介绍了Datagram Scoket双向通信,需要的朋友可以参考下 ...

    Java教程网2412019-11-20
  • JAVA教程java正则表达式获取url的host示例

    java正则表达式获取url的host示例

    使用httpclient抓取页面信息时需要填写HOST,使用此正则提取抓取URL的HOST内容 ...

    java教程网4312019-11-08
  • JAVA教程Spring-data-redis操作redis知识总结

    Spring-data-redis操作redis知识总结

    这篇文章主要介绍了Spring-data-redis操作redis知识总结,spring-data-redis是spring-data模块的一部分,专门用来支持在spring管理项目对redis的操作。...

    醉眼识朦胧5012020-09-11
  • JAVA教程Java基础之如何学好Java

    Java基础之如何学好Java

    这篇文章已经是有数年“网龄”的老文,不过在今天看来仍然经典。如何学习java?本篇文章可以说也是面对编程初学者的一篇指导文章,其中对于如何学习...

    hebedich4962019-12-03
  • JAVA教程Maven 生成打包可执行jar包的方法步骤

    Maven 生成打包可执行jar包的方法步骤

    这篇文章主要介绍了Maven 生成打包可执行jar包的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋...

    荒野雄兵4552020-09-05