zoukankan      html  css  js  c++  java
  • Java中的集合(四)PriorityQueue常用方法

    Java中的集合(四)PriorityQueue常用方法

    PriorityQueue的基本概念等都在上一篇已说明,感兴趣的可以点击 Java中的集合(三)继承Collection的Queue接口 查看

    这里主要以PriorityQueue的常用方法的学习

    一、PriorityQueue的实现

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    上图中给层序遍历编号,从中可以发现父子节点总有如下的关系:

    通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。

    PriorityQueue的peek()和element()操作是常数时间add()、offer()、 无参数的remove()以及poll()方法的时间复杂度都是log(N)

    二、PriorityQueue常用的方法

     

    三、常用方法剖析

    (一)插入元素:add(E e)和offer(E e)

    add(E e)和offer(E e)两者的语义是相同,都是往优先队列中插入元素,只是Queue接口规定了两者对插入失败时采取不同的处理方式。add(E e)方法插入元素失败时会抛出异常,offer(E e)插入元素失败时会返回false,对PriorityQueue而言,两者没有什么区别。

     1 public boolean add(E e) {
     2     return offer(e); // add方法内部调用offer方法
     3 }
     4 public boolean offer(E e) {
     5     if (e == null) // 元素为空的话,抛出NullPointerException异常
     6         throw new NullPointerException();
     7     modCount++;
     8     int i = size;
     9     if (i >= queue.length) // 如果当前用堆表示的数组已经满了,调用grow方法扩容
    10         grow(i + 1); // 扩容
    11     size = i + 1; // 元素个数+1
    12     if (i == 0) // 堆还没有元素的情况
    13         queue[0] = e; // 直接给堆顶赋值元素
    14     else // 堆中已有元素的情况
    15         siftUp(i, e); // 重新调整堆,从下往上调整,因为新增元素是加到最后一个叶子节点
    16     return true;
    17 }
    18 private void siftUp(int k, E x) {
    19     if (comparator != null)  // 比较器存在的情况下
    20         siftUpUsingComparator(k, x); // 使用比较器调整
    21     else // 比较器不存在的情况下
    22         siftUpComparable(k, x); // 使用元素自身的比较器调整
    23 }
    24 private void siftUpUsingComparator(int k, E x) {
    25     while (k > 0) { // 一直循环直到父节点还存在
    26         int parent = (k - 1) >>> 1; // 找到父节点索引,等同于(k - 1)/ 2
    27         Object e = queue[parent]; // 获得父节点元素
    28         // 新元素与父元素进行比较,如果满足比较器结果,直接跳出,否则进行调整
    29         if (comparator.compare(x, (E) e) >= 0) 
    30             break;
    31         queue[k] = e; // 进行调整,新位置的元素变成了父元素
    32         k = parent; // 新位置索引变成父元素索引,进行递归操作
    33     }
    34     queue[k] = x; // 新添加的元素添加到堆中
    35 }
    View Code

    下面根据图解演示插入元素过程:

    (二)、获取元素但不删除队列首元素:element()和peek()

    element()和peek()的语义是相同的,都是获取元素但不删除队列首元素,也就是队列中权值最下的元素,只是Queue接口规定了两者删除元素失败时的不同处理方式,element()会抛出异常,peek()会返回null。根据小顶堆的特性,堆顶最上层的元素权值是最小的,由于是数组实现的,根据小标关系,小标0既是堆顶的元素,也是数组的第一个元素,所以直接返回下标为0的那个元素即可。

     1 // PriorityQueue的peek()
     2 public E peek() {
     3     if (size == 0)
     4         return null;
     5     return (E) queue[0];//0下标处的那个元素就是最小的那个
     6 }
     7 
     8 // AbstractQueue的element(),由于PriorityQueue继承自AbstractQueue,所以可以使用element()方法
     9 public E element() {
    10         E x = peek();
    11         if (x != null)
    12             return x;
    13         else
    14             throw new NoSuchElementException();
    15     }
    View Code

    下面根据图解演示获取元素过程:

    (三)、获取并删除队列首元素:remove()和poll()

    element()和peek()的语义是相同的,都是获取元素并删除队列首元素,只是Queue接口规定了两者删除元素失败时的不同处理方式,remove()会抛出异常,poll()会返回null。由于删除会影响队列的结构,所以会通过siftDown()和siftUp()方法调整队列结构

     1 public E poll() {
     2     if (size == 0)
     3         return null;
     4     int s = --size;
     5     modCount++;
     6     E result = (E) queue[0];//0下标处的那个元素就是最小的那个
     7     E x = (E) queue[s];
     8     queue[s] = null;
     9     if (s != 0)
    10         siftDown(0, x);//调整
    11     return result;
    12 }
    13 
    14 public E remove() {
    15     E x = poll();
    16     if (x != null)
    17         return x;
    18     else
    19         throw new NoSuchElementException();
    20 }
    21 
    22 private void siftDown(int k, E x) {
    23     if (comparator != null) // 比较器存在的情况下
    24         siftDownUsingComparator(k, x); // 使用比较器调整
    25     else // 比较器不存在的情况下
    26         siftDownComparable(k, x); // 使用元素自身的比较器调整
    27 }
    28 private void siftDownUsingComparator(int k, E x) {
    29     int half = size >>> 1; // 只需循环节点个数的一般即可
    30     while (k < half) {
    31         int child = (k << 1) + 1; // 得到父节点的左子节点索引,即(k * 2)+ 1
    32         Object c = queue[child]; // 得到左子元素
    33         int right = child + 1; // 得到父节点的右子节点索引
    34         if (right < size &&
    35             comparator.compare((E) c, (E) queue[right]) > 0) // 左子节点跟右子节点比较,取更大的值
    36             c = queue[child = right];
    37         if (comparator.compare(x, (E) c) <= 0)  // 然后这个更大的值跟最后一个叶子节点比较
    38             break;
    39         queue[k] = c; // 新位置使用更大的值
    40         k = child; // 新位置索引变成子元素索引,进行递归操作
    41     }
    42     queue[k] = x; // 最后一个叶子节点添加到合适的位置
    43 }
    View Code

    下面根据图解演示获取元素过程:


    通过上述代码和图解可以看出:

    1、首先记录0下标处的元素,并用最后一个元素替换0下标位置的元素,

    2、调用siftDown()方法对堆进行调整,最后返回原来0下标处的那个元素(也就是最小的那个元素)。

    重点是siftDown(int k,E e)方法,该方法的作用是k指定的位置开始,将x逐层向下与当前点的左右孩子中较小的那个交换,直到x小于或等于左右孩子中的任何一个为止

    (四)、删除队列中的指定元素:remove(Object o)

    remove(Object o)用于删除队列中的指定元素(如果队列中有多个相同元素,只删除一个),由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。

    public boolean remove(Object o) {
        int i = indexOf(o); // 找到数据对应的索引
        if (i == -1) // 不存在的话返回false
            return false;
        else { // 存在的话调用removeAt方法,返回true
            removeAt(i);
            return true;
        }
    }
    private E removeAt(int i) {
        modCount++;
        int s = --size; // 元素个数-1
        if (s == i) // 如果是删除最后一个叶子节点
            queue[i] = null; // 直接置空,删除即可,堆还是保持特质,不需要调整
        else { // 如果是删除的不是最后一个叶子节点
            E moved = (E) queue[s]; // 获得最后1个叶子节点元素
            queue[s] = null; // 最后1个叶子节点置空
            siftDown(i, moved); // 从上往下调整
            if (queue[i] == moved) { // 如果从上往下调整完毕之后发现元素位置没变,从下往上调整
                siftUp(i, moved); // 从下往上调整
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }
    View Code

    具体来说,remove(Object o)可以分为2种情况:

    1. 删除的是最后一个元素。直接删除即可,不需要调整。

    2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用siftDown()或siftUp()。

    1. 删除的是最后一个元素。直接删除即可,不需要调整。

    下面根据图解演示获取元素过程:


    2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用siftDown()或siftUp()。

    下面根据图解演示获取元素过程:


    四、PriorityBlockingQueue

    (一)、简介

    PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但需要注意的是不能保证同优先级元素的顺序。

    (二)、四种构造方法

    (三)、定义属性、数据结构

    (四)、以offer(E e)方法说明与PriorityQueue的不同

    插入元素源码:

     1 public boolean offer(E e) {
     2     if (e == null)
     3         throw new NullPointerException();
     4     final ReentrantLock lock = this.lock;
     5     lock.lock();
     6     int n, cap;
     7     Object[] array;
     8     while ((n = size) >= (cap = (array = queue).length))
     9         tryGrow(array, cap);
    10     try {
    11         Comparator<? super E> cmp = comparator;
    12         if (cmp == null)
    13             siftUpComparable(n, e, array);
    14         else
    15             siftUpUsingComparator(n, e, array, cmp);
    16         size = n + 1;
    17         notEmpty.signal();
    18     } finally {
    19          lock.unlock();
    20     }
    21     return true;
    22 } 
    View Code

    和PriorityQueue的实现基本一致区别就是在于加锁了,并发出了非空信号唤醒阻塞的获取线程。

    通过tryGrow(Object[] array, int oldCap)扩容队列

     1     private void tryGrow(Object[] array, int oldCap) {
     2         lock.unlock(); // must release and then re-acquire main lock
     3         Object[] newArray = null;
     4         if (allocationSpinLock == 0 &&
     5             UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
     6                                      0, 1)) {
     7             try {
     8                 int newCap = oldCap + ((oldCap < 64) ?
     9                                        (oldCap + 2) : // grow faster if small
    10                                        (oldCap >> 1));
    11                 if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
    12                     int minCap = oldCap + 1;
    13                     if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
    14                         throw new OutOfMemoryError();
    15                     newCap = MAX_ARRAY_SIZE;
    16                 }
    17                 if (newCap > oldCap && queue == array)
    18                     newArray = new Object[newCap];
    19             } finally {
    20                 allocationSpinLock = 0;
    21             }
    22         }
    23         if (newArray == null) // back off if another thread is allocating
    24             Thread.yield();
    25         lock.lock();
    26         if (newArray != null && queue == array) {
    27             queue = newArray;
    28             System.arraycopy(array, 0, newArray, 0, oldCap);
    29         }
    30     }
    View Code

    从源码可以看出:为了更好的并发性,其先释放了全局锁,然后通过UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,0, 1))设置allocationSpinLockOffset来判断哪个线程获得扩容权限,如果没有获得权限,就放开CPU资源。后面扩容操作是通过简单的乐观锁allocationSpinLock来进行控制的。

    (五)、小结

    1、在多线程环境下,可以使用PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。

    2、由于和PriorityQueue都是继承自AbstractQueue,所以其它的操作过程都和PriorityQueue的类似,只是定义的方法都使用 ReentrantLock 锁来保持同步。

    五、题外总结

    1、jdk内置的优先队列PriorityQueue内部使用一个堆维护数据,每当有数据add进来或者poll出去的时候会对堆做从下往上的调整和从上往下的调整。

    2、PriorityQueue不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞

     

  • 相关阅读:
    TCP性能调优
    Qt 实现应用程序单实例运行
    table多选
    多选删除最佳处理
    获取路由或路径
    当前页面打开新页面
    vue版本更新index.html缓存
    vue项目js和css文件名避免浏览器缓存再vue.config.js中配置
    vue动态表格
    IE网页被缓存,get接口缓存
  • 原文地址:https://www.cnblogs.com/lingq/p/12735318.html
Copyright © 2011-2022 走看看