【java容器的刻意练习】【十八】PriorityQueue的删除源码分析

上一篇讲完PriorityQueue的添加元素是以平衡二叉堆的元素“上浮”来实现。

这一篇分析下PriorityQueue的删除操作poll

    public E poll() {
        final Object[] es;
        final E result;

        // 取出数组第一个元素,即是根节点
        if ((result = (E) ((es = queue)[0])) != null) {
            // 操作次数加1
            modCount++;
            final int n;

            // 取出最后一个元素x,数组长度减一
            final E x = (E) es[(n = --size)];

            // 将数组最后一个位置置空
            es[n] = null;
            
            // 如果数组还有元素
            if (n > 0) {
                // 删除根节点,通过“下浮”调整堆,使其有序
                final Comparator<? super E> cmp;
                if ((cmp = comparator) == null)
                    siftDownComparable(0, x, es, n);
                else
                    siftDownUsingComparator(0, x, es, n, cmp);
            }
        }
        // 返回根节点元素
        return result;
    }

offer的“上浮”差不多,poll弄出来一个“下沉”。

  /**
   * 堆的"下沉"调整.
   * 删除数组在位置 k 对应的元素,并重新调整堆使其有序
   *
   * @param k     待删除的位置,也可以理解为空位
   * @param x     待插入的元素
   * @param es   堆数组
   * @param n     堆的大小
   */
    private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
        // assert n > 0;
        Comparable<? super T> key = (Comparable<? super T>)x;

        // 相当于n除2, 即找到索引n对应结点的父结点位置
        int half = n >>> 1;           // loop while a non-leaf

        // 待删除位置小于父节点,才需要循环下沉调整
        // 否则待删除位置可以直接覆盖
        while (k < half) {

            /**
             * 下述代码中:
             * c保存k的左右子结点中的较小结点值 
             * child保存较小结点对应的数组下标
             */
            int child = (k << 1) + 1; // 找出k的左节点对应的数组下标
            Object c = es[child];
            int right = child + 1;
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
                // 右节点在堆大小内,并且左节点比右节点大
                // 那么c保存左节点(即是较小的节点)
                c = es[child = right];

            // 将待插入元素与k节点的较小子节点比较        
            if (key.compareTo((T) c) <= 0)
                // 如果待插入元素更小,证明找到空位k,直接退出循环
                break;
            
            // 如果待插入元素更大,那么较小子节点上去补位
            es[k] = c;
            // 空位k下沉
            k = child;
        }

        // 找到空位k,插入元素
        es[k] = key;
    }

上述代码其实是经典的堆“下沉”操作,对堆中某个顶点下沉,步骤如下:

  • 找到该顶点的左右子结点中较小的那个;
  • 与当前结点交换;
  • 重复前2步直到当前结点没有左右子结点或比左右子结点都小。
i堆的下沉

空位置的下沉操作,类似我们抢前排座位。当第一排有人走了,坐第二排的小个子一点的同学就赶紧坐上去,空出了第二排他原来坐的位子。然后后面小个子也跟着坐上来。一直到新进来的同学也有合适的座位为止。

因为“下沉”也是折半法操作,所以时间复杂度也是O(logN)。

好,我们来总结下PriorityQueue的poll操作:

  • 空位的下沉的过程,可以想象学生抢前排座位,比较矮那位先坐上去。
  • 下沉算法的关键是找出较小的子节点,再与待插入的元素比较优先级,来决定空位让给谁。
  • 时间复杂度是Ο(logN)

推荐阅读更多精彩内容