JDK源码阅读—PriorityQueue

简介

PriorityQueue是一个优先队列的实现,每次出队都会弹出最小或最大的元素,其底层数据结构为二叉堆,使用数组存放二叉堆的元素。

以下分析基于corretto-1.8.0_282版本。

继承关系

PriorityQueue继承关系.png

  1. 实现了Serializable接口,可以被序列化。
  2. 实现了Queue接口,可以用作队列。

属性

DEFAULT_INITIAL_CAPACITY

/**
 * 默认初始容量
 */
private static final int DEFAULT_INITIAL_CAPACITY = 11;

queue

/**
 * 二叉堆存放元素的数组,若queue[n]为父节点,则其两个子
 * 节点为queue[2 * (n + 1)]和queue[2 * (n + 2)],
 * 若queue[n]为子节点,则其父节点为queue[(n - 1) / 2]
 * 索引为0处称为堆顶
 */
transient Object[] queue;

size

/**
 * 元素个数
 */
private int size = 0;

comparator

/**
 * 比较器,未设置时会使用元素的自然序
 */
private final Comparator<? super E> comparator;

modCount

/**
 * 修改次数,用作快速失败检查
 */
transient int modCount = 0;

构造方法

PriorityQueue()

/**
 * 使用默认初始容量实例化PriorityQueue
 */
public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

PriorityQueue(int initialCapacity)

/**
 * 使用给定的初始容量实例化PriorityQueue
 */
public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}

PriorityQueue(Comparator<? super E> comparator)

/**
 * 使用默认初始容量和给定的比较器实例化PriorityQueue
 */
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

PriorityQueue(int initialCapacity, Comparator<? super E> comparator)

/**
 * 使用给定的初始容量和比较器实例化PriorityQueue
 */
public PriorityQueue(int initialCapacity,
                        Comparator<? super E> comparator) {
    // 初始容量必须大于1
    if (initialCapacity < 1)
        throw new IllegalArgumentException();

    // 属性赋值
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

PriorityQueue(Collection<? extends E> c)

/**
 * 使用给定集合实例化PriorityQueue
 * 若给定集合是SortedSet的一个实例或是另一个PriorityQueue,则元素会按相同的顺序保存,
 * 否则会按元素自然序保存
 */
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        // 比较器配置为SortedSet的比较器
        this.comparator = (Comparator<? super E>) ss.comparator();
        // 复制元素
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        // 比较器配置为给定的PriorityQueue的比较器
        this.comparator = (Comparator<? super E>) pq.comparator();
        // 复制元素
        initFromPriorityQueue(pq);
    }
    else {
        // 比较器设置为null
        this.comparator = null;
        // 复制元素
        initFromCollection(c);
    }
}

/**
 * 将给定集合中的元素复制过来
 */
private void initElementsFromCollection(Collection<? extends E> c) {
    // 集合转数组
    Object[] a = c.toArray();

    // 集合不是ArrayList类型,则数组可能不是Object[]类型,需要做强制类型转换
    if (c.getClass() != ArrayList.class)
        a = Arrays.copyOf(a, a.length, Object[].class);

    int len = a.length;

    // 集合中不能有null元素
    if (len == 1 || this.comparator != null)
        for (int i = 0; i < len; i++)
            if (a[i] == null)
                throw new NullPointerException();

    // 属性赋值
    this.queue = a;
    this.size = a.length;
}

/**
 * 将给定PriorityQueue中的元素复制过来
 */
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
    if (c.getClass() == PriorityQueue.class) {
        // 类型完全相同,则直接复制数组和大小
        this.queue = c.toArray();
        this.size = c.size();
    } else {
        // c是PriorityQueue的子类,则按普通集合来复制
        initFromCollection(c);
    }
}

/**
 * 将给定集合中的元素复制过来
 * 给定集合可能不是有序的,复制后需要重新堆化
 */
private void initFromCollection(Collection<? extends E> c) {
    // 复制元素
    initElementsFromCollection(c);
    // 堆化
    heapify();
}

/**
 * 将无序数组调整为二叉堆
 * 从最后一个非叶子节点开始向根节点遍历,将每个节点下沉到正确的位置
 */
@SuppressWarnings("unchecked")
private void heapify() {
    // (size / 2) - 1 是最后一个非叶子节点的索引
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        // 下沉,详细查看下面的poll()方法的注释
        siftDown(i, (E) queue[i]);
}

方法

offer(E e)

/**
 * 插入元素
 * 以最小堆为例,插入流程如下
 * 1.将元素放在[size - 1]处,即最后一个元素,此时索引记做k
 * 2.找到k的父节点,父节点索引parent为(k - 1) / 2
 * 3.比较queue[k]与queue[parent]的大小,若queue[k] >= queue[parent],
 *   满足父节点小于等于子节点的条件,则k是正确的位置,插入完成。
 * 4.若queue[k] < queue[parent],不满足父节点小于等于子节点的条件,交换这两个节点,
 *   然后令k = parent,之后回到第2步重复。
 *
 * 简单来说就是将元素插入二叉堆的末尾,然后与父节点比较,若子节点小于父节点,交换这两个父子节点,
 * 再与父节点比较大小,直到找到一个满足子节点大于等于父节点的位置,插入完成
 */
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;
}

/**
 * 扩容
 */
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // 若旧容量小于64,新容量为原来的2倍+2,否则新容量为原来的1.5倍
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                        (oldCapacity + 2) :
                                        (oldCapacity >> 1));
    // 新容量不能超过一个上限
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 对数组进行扩容
    queue = Arrays.copyOf(queue, newCapacity);
}

/**
 * 计算容量上限
 */
private static int hugeCapacity(int minCapacity) {
    // 容量太大,溢出,抛出异常
    if (minCapacity < 0)
        throw new OutOfMemoryError();

    // 若需要的容量大于MAX_ARRAY_SIZE则返回Integer.MAX_VALUE,
    // 否则返回MAX_ARRAY_SIZE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

/**
 * 在索引k处插入元素,并上浮到正确的位置
 */
private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

/**
 * 使用元素自然序
 */
@SuppressWarnings("unchecked")
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;
}

/**
 * 使用给定的比较器
 * 逻辑与siftUpComparable相同,只是更换了比较器
 */
@SuppressWarnings("unchecked")
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;
}

poll()

/**
 * 弹出队首元素
 */
@SuppressWarnings("unchecked")
public E poll() {
    // 队列为空返回null
    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;
}

/**
 * 在索引k处插入元素,并下沉到正确的位置
 */
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

/**
 * 使用元素自然序
 */
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;

    // 处理到最后一个非叶子节点,叶子节点不需要比较了
    int half = size >>> 1;
    while (k < half) {
        // 左孩子索引为2k + 1
        int child = (k << 1) + 1;
        // 左孩子赋给变量c
        Object c = queue[child];

        // 右孩子索引为2k + 2
        int right = child + 1;

        // 如果右孩子存在且左孩子大于右孩子
        // 则将c赋值为右孩子
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];

        // 此时,变量c为左右孩子中最小的那个
        // 若给定节点小于等于c,则说明已经下沉到正确的位置,停止循环
        if (key.compareTo((E) c) <= 0)
            break;

        // 节点比左右孩子中最小的那个还大,则交换父子节点,继续下次比较
        queue[k] = c;
        k = child;
    }

    // 在正确的位置插入给定元素
    queue[k] = key;
}

/**
 * 使用给定的比较器
 * 逻辑与siftDownComparable方法相同,只是更换了比较器
 */
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    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;
}

peek()

/**
 * 若队列不为空,则返回堆顶(索引为0的元素)元素
 * @return
 */
@SuppressWarnings("unchecked")
public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

remove(Object o)

/**
 * 删除给定的元素
 * 使用equals比较
 */
public boolean remove(Object o) {
    // 查找给定元素的索引
    int i = indexOf(o);

    if (i == -1)
        // 给定元素不存在
        return false;
    else {
        // 删除
        removeAt(i);
        return true;
    }
}

/**
 * 删除给定索引处的元素,删除后可能会留下一个空位,
 * 会将最后一个元素替换到删除位置,经过下沉和上浮(可能)操作后,使树重新平衡
 */
@SuppressWarnings("unchecked")
private E removeAt(int i) {
    // 修改次数加一
    modCount++;

    // 元素个数减一
    int s = --size;
    if (s == i)
        // 删除的最后一个元素,则直接将索引i处置为null即可
        queue[i] = null;
    else {
        // 将队列最后一个元素插入i处,并下沉到正确的位置
        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;
}

contains(Object o)

/**
 * 判断队列中是否包含给定元素
 */
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

/**
 * 查找给定元素的索引,若元素不存在则返回-1
 */
private int indexOf(Object o) {
    if (o != null) {
        // 遍历元素,返回第一个equals的元素的索引
        for (int i = 0; i < size; i++)
            if (o.equals(queue[i]))
                return i;
    }

    // 找不到返回-1
    return -1;
}

clear()

/**
 * 清空队列
 */
public void clear() {
    // 修改次数加一
    modCount++;

    // 将数组所有位置置为null
    for (int i = 0; i < size; i++)
        queue[i] = null;

    // 元素个数置为0
    size = 0;
}

总结

  1. PriorityQueue默认初始容量为11,数组容量不足时会自动扩容,容量小于64时扩容为原来的2倍+2,容量不小于64时扩容为原来的1.5倍。

  2. PriorityQueue底层数据结构为二叉堆,使用数组来存放树的节点,节点关系可通过简单计算得到。

    若queue[n]为父节点,则其两个子节点为queue[2 * (n + 1)]和queue[2 * (n + 2)]

    若queue[n]为子节点,则其父节点为queue[(n - 1) / 2]

  3. PriorityQueue中元素不是有序的,只是堆顶存放着最大或最小的元素。

  4. PriorityQueue非线程安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值