一.数据结构

Priority queue 是一个 平衡二项堆(平衡二叉树);树中所有的子节点必须大于等于父节点,而无需维护大小关系,是一个最小堆
PriorityQueue源码解析
- 父节点与子节点的索引关系:
① 假设父节点为queue[n],那么左孩子节点为queue[2n+1],右孩子节点为queue[2(n+1)]
② 假设孩子节点(无论是左孩子节点还是右孩子节点)为queue[n],n>0。那么父节点为queue[(n-1) >>> 1]

- 叶子节点和非叶子节点:
① 一个长度为size的优先级队列,当index >= size >>> 1时,该节点为叶子节点。否则,为非叶子节点

二.常用操作

2.1 插入节点

add 和offer 都是添加节点, add 就是调用offer 接口 来实现的。插入的时间复杂度为O(log(n))

    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)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }

代码中 有两个需要注意的点:

1. 扩容
如果oldCapacity 比较小,小于 64, 则double,申请两倍的; 否则提升50%.

       int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));

申请新的容量之后需要 对newCapacity 进行 处理,看是否 上下溢出。

Jvm 数组 最大的容量限制:

   /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

Integer 的上限:

    /**
     * A constant holding the maximum value an {@code int} can
     * have, 2<sup>31</sup>-1.
     */
    @Native public static final int   MAX_VALUE = 0x7fffffff;

调整后的容量 :

        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

3. siftUp 最小堆化
插入节点的时候,要保证最小堆的结构,要求仍能满足第一部分中的最小堆的特点,需要堆化:
通过“int parent = (k - 1) >>> 1;”获取到当前要插入节点位置的父节点,比较父节点和待插入节点,如果待插入节点小于父节点,则将父节点插入到子节点的位置,然后在获取父节点的父节点循环上面的操作,直到待插入节点大于等于父节点,则在相应位置插入这个节点。最终保证代表优先级队列的平衡二叉树中,所有的子节点都大于它们的父节点,但同一层的子节点间并不需要维护大小关系。

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

2.2 删除元素

    private E removeAt(int i) {
        // assert i >= 0 && i < size;
        modCount++;
        int s = --size;
        if (s == i) // removed last element
            queue[i] = null;
        else {
            E moved = (E) queue[s];
            queue[s] = null;
            siftDown(i, moved);
            if (queue[i] == moved) {
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }

删除元素分三种情况:

  1. 删除的为队尾元素, 直接 queue[i] = null;
  2. 删除的为 叶子节点元素:
    上面代码中 先执行siftDown, siftDown 只对 非叶子节点 有效:
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;   // 在第一节中说过,k < half 的可以 认为是 非叶子节点
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

所以此处只会执行 queue[k] = x, 将 队尾节点 放在 K 位置,然后 执行siftUp, 即 前面讲的siftUp 最小化堆的过程, 将该节点与其父节点进行比较, 只要其比父节点大,就往上移

  1. 删除的为 非叶子节点元素, 时间复杂度为O(n)+2O(log(n)):
    有两部需要处理: siftDown 和 siftUp。
    siftDown: 先将 队尾节点放到 要删除的节点处,然后 比较该节点与其孩子节点的大小,如果他比孩子节点大,则下沉, 一直循环下去,找到最终该节点的存放位置;
    siftUp: 再将经过 siftDown 处理后的该节点进行提升,将其与父节点进行比较,如果比父节点小,则进行提升。

删除示例:
PriorityQueue源码解析
PriorityQueue源码解析

三.应用

求TopK 问题:如何在一个包含n个数据的 数组中 查找前K 个最大的?
我们可以维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出的元素与堆顶元素 比较,如果比堆顶元素大,我们就插入堆中,依次循环,直到最后结束,堆中的元素即为topk ,代码

参考

1.PriorityQueue 源码分析

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2021-06-11
  • 2022-02-24
  • 2021-11-25
  • 2021-11-07
  • 2020-11-13
  • 2021-05-16
猜你喜欢
  • 2020-05-13
  • 2021-09-27
  • 2022-01-05
  • 2021-09-25
  • 2022-01-11
相关资源
相似解决方案