PriorityQueue深入解析

转载 2015年07月10日 16:54:01

PriorityQueue介绍

    在平时的编程工作中似乎很少碰到PriorityQueue(优先队列) ,故很多人一开始看到优先队列的时候还会有点迷惑。优先队列本质上就是一个最小堆。前面一篇文章介绍了堆排序和堆的性质。而堆又是什么呢?它是一个数组,不过满足一个特殊的性质。我们以一种完全二叉树的视角去看这个数组,并用二叉树的上下级关系来映射到数组上面。如果是最大堆,则二叉树的顶点是保存的最大值,最小堆则保存的最小值。

    下面是一个典型的优先队列的结构图:

 

    它的每个父节点都比两个子节点要小,但是整个数组又不是完全顺序的。

    有了前面的这些铺垫,我们再来看它的整体结构和各种调整过程。

建堆

   PriorityQueue内部的数组声明如下:

Java代码  收藏代码
  1. private transient Object[] queue;  
  2.   
  3. private static final int DEFAULT_INITIAL_CAPACITY = 11;  

   它默认的长度为11. 建堆的过程中需要添加新的元素,

    PriorityQueue的建堆过程和最大堆的建堆过程基本上一样的,从有子节点的最靠后元素开始往前,每次都调用siftDown方法来调整。这个过程也叫做heapify。

 

Java代码  收藏代码
  1. private void heapify() {  
  2.         for (int i = (size >>> 1) - 1; i >= 0; i--)  
  3.             siftDown(i, (E) queue[i]);  
  4.     }  
  5.   
  6. private void siftDown(int k, E x) {  
  7.         if (comparator != null)  
  8.             siftDownUsingComparator(k, x);  
  9.         else  
  10.             siftDownComparable(k, x);  
  11.     }  
  12.   
  13. private void siftDownComparable(int k, E x) {  
  14.         Comparable<? super E> key = (Comparable<? super E>)x;  
  15.         int half = size >>> 1;        // loop while a non-leaf  
  16.         while (k < half) {  
  17.             int child = (k << 1) + 1// assume left child is least  
  18.             Object c = queue[child];  
  19.             int right = child + 1;  
  20.             if (right < size &&  
  21.                 ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)  
  22.                 c = queue[child = right];  
  23.             if (key.compareTo((E) c) <= 0)  
  24.                 break;  
  25.             queue[k] = c;  
  26.             k = child;  
  27.         }  
  28.         queue[k] = key;  
  29.     }  
  30.   
  31.     private void siftDownUsingComparator(int k, E x) {  
  32.         int half = size >>> 1;  
  33.         while (k < half) {  
  34.             int child = (k << 1) + 1;  
  35.             Object c = queue[child];  
  36.             int right = child + 1;  
  37.             if (right < size &&  
  38.                 comparator.compare((E) c, (E) queue[right]) > 0)  
  39.                 c = queue[child = right];  
  40.             if (comparator.compare(x, (E) c) <= 0)  
  41.                 break;  
  42.             queue[k] = c;  
  43.             k = child;  
  44.         }  
  45.         queue[k] = x;  
  46.     }  
    siftDown的过程是将一个节点和它的子节点进行比较调整,保证它比它所有的子节点都要小。这个调整的顺序是从当前节点向下,一直调整到叶节点。

 

    siftDown的过程如下图所示:

    因为一开始要建堆的时候,里面的元素是杂乱无章的,所以调整前可能的结构如下:

    假设我们siftDown的元素是9,则它先和它的左右子节点进行比较,然后和最小的子节点交换位置:

 

    经过一次交换之后,发现它还有子节点,而且子节点比它小,所以需要继续交换。

    按照这个思路来理解,前面的过程就很明了了。

扩展

    堆里面的数组长度不是固定不变的,如果不断往里面添加新元素的时候,也会面临数组空间不够的情形,所以也需要对数组长度进行扩展。这个扩展方法的源头就是由插入元素引起的。

数组长度扩展的方法如下:

Java代码  收藏代码
  1. private void grow(int minCapacity) {  
  2.         int oldCapacity = queue.length;  
  3.         // Double size if small; else grow by 50%  
  4.         int newCapacity = oldCapacity + ((oldCapacity < 64) ?  
  5.                                          (oldCapacity + 2) :  
  6.                                          (oldCapacity >> 1));  
  7.         // overflow-conscious code  
  8.         if (newCapacity - MAX_ARRAY_SIZE > 0)  
  9.             newCapacity = hugeCapacity(minCapacity);  
  10.         queue = Arrays.copyOf(queue, newCapacity);  
  11.     }  
  12.   
  13.     private static int hugeCapacity(int minCapacity) {  
  14.         if (minCapacity < 0// overflow  
  15.             throw new OutOfMemoryError();  
  16.         return (minCapacity > MAX_ARRAY_SIZE) ?  
  17.             Integer.MAX_VALUE :  
  18.             MAX_ARRAY_SIZE;  
  19.     }  

    这部分代码和ArrayList的内部实现代码基本相同,都是先找到合适的数组长度,然后将元素从旧的数组拷贝到新的数组。

    而添加新元素的过程如下:

Java代码  收藏代码
  1. public boolean offer(E e) {  
  2.         if (e == null)  
  3.             throw new NullPointerException();  
  4.         modCount++;  
  5.         int i = size;  
  6.         if (i >= queue.length)  
  7.             grow(i + 1);  
  8.         size = i + 1;  
  9.         if (i == 0)  
  10.             queue[0] = e;  
  11.         else  
  12.             siftUp(i, e);  
  13.         return true;  
  14.     }  

    每次添加新的元素进来实际上是在数组的最后面增加。在增加这个元素的时候就有了判断数组长度和调整的一系列动作。等这些动作调整完之后就要进行siftUp方法调整。这样做是为了保证堆原来的性质。

    siftUp的过程可以用如下图来描述:

    假设我们在后面添加了元素3,那么我们就要将这个元素和它的父节点比较,如果它比它的父节点小,则要交换他们的顺序。这样一直重复到它大于等于它的父节点或者到根节点。

    经过调整之后,则变成符合条件的最小堆:‘

     它的详细实现代码如下:

Java代码  收藏代码
  1. private void siftUp(int k, E x) {  
  2.         if (comparator != null)  
  3.             siftUpUsingComparator(k, x);  
  4.         else  
  5.             siftUpComparable(k, x);  
  6.     }  
  7.   
  8.     private void siftUpComparable(int k, E x) {  
  9.         Comparable<? super E> key = (Comparable<? super E>) x;  
  10.         while (k > 0) {  
  11.             int parent = (k - 1) >>> 1;  
  12.             Object e = queue[parent];  
  13.             if (key.compareTo((E) e) >= 0)  
  14.                 break;  
  15.             queue[k] = e;  
  16.             k = parent;  
  17.         }  
  18.         queue[k] = key;  
  19.     }  
  20.   
  21.     private void siftUpUsingComparator(int k, E x) {  
  22.         while (k > 0) {  
  23.             int parent = (k - 1) >>> 1;  
  24.             Object e = queue[parent];  
  25.             if (comparator.compare(x, (E) e) >= 0)  
  26.                 break;  
  27.             queue[k] = e;  
  28.             k = parent;  
  29.         }  
  30.         queue[k] = x;  
  31.     }  

注意事项:

    我们看前面的调整方法不管是siftUp还是siftDown都用了两个方法,一个是用的comparator,还有一个是用的默认比较结果。这样做的目的是考虑到我们要比较的元素不仅仅是数字等类型,也有可能是被定义了可比较的数据类型。对于自定义的数据类型,他们的大小比较定义需要实现comparator接口。至于使用comparator接口的意义背后的思想,可以参照我的这一篇博文

总结

    PriorityQueue本质上就是堆排序里面建的最小堆。最小堆满足的一个基本性质是堆顶端的元素是所有元素里最小的那个。如果我们将顶端的元素去掉之后,为了保持堆的性质,需要进行调整。对堆的操作和调整主要包含三个方面,增加新的元素,删除顶端元素和建堆时保证堆性质的操作。前面讨论堆排序的文章已经有了一个详细的介绍。这里主要结合jdk的类库实现,看看一个用于实际生产环境的优先队列实现。 另外,PriorityQueue在一些经典算法中也有得到应用,相当于是它们实现的基础。

相关文章推荐

PriorityQueue优先队列实现原理

一、什么是优先队列    优先队列不是按照普通对象先进先出原FIFO则进行数据操作,其中的元素有优先级属性,优先级高的元素先出队。本文提到的PriorityQueue队列,是基于最小堆原理实现。 ...

PriorityQueue实现原理

PriorityQueue PriorityQueue是个基于优先级堆的极大优先级队列。 此队列按照在构造时所指定的顺序对元素排序,既可以根据元素的自然顺序来指定排序(参阅 Comparable), ...

关于priority_queue

priority_queue允许用户为队列中元素设置优先级,放置元素的时候不是直接放到队尾,而是放置到比它优先级低的元素前面,标准库默认使用 priority_queue模板类有三个模板参数:元素类型...

STL系列之五 priority_queue 优先级队列

priority_queue 优先级队列是一个拥有权值概念的单向队列queue,在这个队列中,所有元素是按优先级排列的(也可以认为queue是个按进入队列的先后做为优先级的优先级队列——先进入队列的元...

PriorityQueue详解

在Java SE 5.0中,引入了一些新的Collection API,PriorityQueue就是其中的一个。今天由于机缘巧合,花了一个小时看了一下这个类的内部实现,代码很有点意思,所以写下来跟大...

java中PriorityQueue优先级队列使用方法

优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。   PriorityQueue是从JDK1.5开始提供的新的数据结构接口。   如果不提供Comparato...

PriorityQueue

PriorityQueue PriorityQueue是个基于优先级堆的极大优先级队列。 此队列按照在构造时所指定的顺序对元素排序,既可以根据元素的自然顺序来指定排序(参阅 Comparable)...
  • hudashi
  • hudashi
  • 2011年11月07日 11:21
  • 14045

priority_queue用法小结

刚开始学习算法不久,一些常用的算法工具还没有掌握,真是丢人!前一段时间用到优先级队列时,都是自己手动通过最大堆或者最小堆来写一个,容易出错且耗时。接触到STL后,开始用map和set模拟一个优先级队列...

priority_queue,以及运算符重载

为了研究priority_queue,我们先写一段错误代码:#include #include using namespace std;struct Node{int key;int value;};...

C++ STL priority_queue

priority_queue 对于基本类型的使用方法相对简单。他的模板声明带有三个参数,priority_queue Type 为数据类型, Container 为保存数据的容器,Functiona...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:PriorityQueue深入解析
举报原因:
原因补充:

(最多只允许输入30个字)