网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

PriorityQueue ​

字数: 0 字 时长: 0 分钟

1. PriorityQueue 概述 ​

PriorityQueue 是 Java 中基于:

text
优先级堆

实现的优先队列。

它能够以较高效率完成元素的插入和删除,并自动维护元素之间的优先级关系。

插入和删除操作的时间复杂度通常为:

text
O(log n)

普通队列一般遵循:

text
FIFO
先进先出

而 PriorityQueue 并不是按照元素进入队列的先后顺序出队,而是:

按照元素的优先级决定出队顺序。

也就是说:

text
元素入队
    ↓
根据优先级调整位置
    ↓
维护堆结构


元素出队
    ↓
优先级最高的元素先出队

1.1 基本使用 ​

例如:

java
public static void main(
        String[] args) {

    // 创建 PriorityQueue
    PriorityQueue<String> priorityQueue =
            new PriorityQueue<>();

    // 添加元素
    priorityQueue.offer(
            "灭绝师太"
    );

    priorityQueue.offer(
            "周芷若"
    );

    priorityQueue.offer(
            "张无忌"
    );

    // 按优先级依次取出元素
    System.out.println(
            "PriorityQueue 中的元素:"
    );

    while (
            !priorityQueue.isEmpty()
    ) {

        System.out.print(
                priorityQueue.poll()
                        + " "
        );
    }
}

输出:

text
PriorityQueue 中的元素:
周芷若 张无忌 灭绝师太

首先创建:

java
PriorityQueue<String>

然后通过:

java
offer()

添加三个元素。

最后不断调用:

java
poll()

按照 PriorityQueue 的优先级顺序依次取出元素。

1.2 自定义优先级 ​

再来看一个例子:

java
PriorityQueue<String> priorityQueue =
        new PriorityQueue<>(
                Comparator.reverseOrder()
        );

priorityQueue.offer(
        "灭绝师太"
);

priorityQueue.offer(
        "周芷若"
);

priorityQueue.offer(
        "张无忌"
);

System.out.println(
        "PriorityQueue 中的元素:"
);

while (
        !priorityQueue.isEmpty()
) {

    System.out.print(
            priorityQueue.poll()
                    + " "
    );
}

这里使用:

java
Comparator.reverseOrder()

指定 PriorityQueue 按照与自然顺序相反的规则进行比较。

输出:

text
PriorityQueue 中的元素:
灭绝师太 张无忌 周芷若

和前一个例子的结果正好相反。

所以 PriorityQueue 中元素之间的优先级,可以通过:

text
自然顺序

或者

Comparator

决定。

2. PriorityQueue 的作用 ​

PriorityQueue 的主要作用是:

维护一组元素的优先级,使元素能够按照指定的优先级顺序被取出。

当调用:

java
poll()

时,会从队首取出:

text
按照当前比较规则优先级最高的元素

PriorityQueue 可以用于:

text
任务调度

事件处理

需要按照优先级处理数据的算法

在一些经典算法中,也经常会使用 PriorityQueue,例如:

text
Dijkstra 算法

Prim 算法

Huffman 编码

2.1 Dijkstra 算法 ​

Dijkstra 算法是一种用于计算:

text
带权图最短路径

的算法。

它采用贪心策略:

text
选择当前距离源点最近的顶点
    ↓
以该顶点为中心进行扩展
    ↓
更新其他顶点的距离
    ↓
继续选择当前最近的顶点

经过多次扩展,可以得到源点到其他顶点的最短路径。

2.2 Prim 算法 ​

Prim 算法用于求解:

text
最小生成树

在一个加权连通图中,寻找一棵生成树,使得:

text
所有边的权值之和最小

基本过程:

text
从任意顶点开始
    ↓
不断扩展生成树
    ↓
每次选择距离当前生成树最近的顶点
    ↓
加入生成树

2.3 Huffman 编码 ​

Huffman 编码是一种基于:

text
霍夫曼树

的压缩算法。

主要思想:

text
出现频率高的字符
    ↓
使用较短编码


出现频率低的字符
    ↓
使用较长编码

从而达到:

text
压缩数据

的目的。

解压缩时再根据编码逐步恢复原始内容。

由于 PriorityQueue 底层基于:

text
堆

实现,因此在数据量较大时,也可以保持较好的时间复杂度。

3. PriorityQueue 与堆 ​

PriorityQueue 中涉及元素之间的:

text
大小关系

优先级关系

元素之间的比较可以通过:

text
自然顺序
Natural Ordering

也可以通过:

text
Comparator

决定。

元素本身也可以实现:

java
Comparable

接口来定义自然顺序。

PriorityQueue 内部通过:

text
堆

维护元素之间的优先级关系。

默认情况下,可以理解为:

text
小顶堆

也可以通过 Comparator 改变比较规则,实现类似:

text
大顶堆

的效果。

3.1 完全二叉树 ​

堆本质上是一棵:

text
完全二叉树
Complete Binary Tree

完全二叉树的特点是:

text
除最后一层以外
    ↓
其他层全部填满


最后一层
    ↓
节点从左向右排列

例如:

text
        1
      /   \
     2     3
    / \   /
   4   5 6

3.2 小顶堆和大顶堆 ​

堆是一种完全二叉树。

小顶堆:

text
父节点
    ↓
小于或等于子节点

因此:

text
根节点
    ↓
整个堆中最小的元素

例如:

text
      1
     / \
    2   3
   / \ / \
  4  5 6  7

这是一个小顶堆。

大顶堆则相反:

text
父节点
    ↓
大于或等于子节点

根节点是最大的元素。

例如:

text
        8
      /   \
     7     5
    / \   / \
   6   4 2   1

3.3 使用数组表示堆 ​

因为完全二叉树的结构非常规则,所以:

堆可以直接使用数组保存,而不需要额外保存左右子节点引用。

例如:

text
[10, 20, 15, 30, 40]

对应的完全二叉树:

text
      10
     /  \
   20    15
  /  \
30   40

如果使用从 0 开始的数组下标:

text
        0
      /   \
     1     2
    / \
   3   4

父子节点之间存在固定关系:

text
leftIndex
=
parentIndex * 2 + 1


rightIndex
=
parentIndex * 2 + 2


parentIndex
=
(nodeIndex - 1) / 2

例如:

text
parentIndex = 0

那么:

text
leftIndex
=
0 × 2 + 1
=
1


rightIndex
=
0 × 2 + 2
=
2

所以:

text
数组下标

和:

text
完全二叉树中的节点位置

可以直接对应起来。

假设数组:

text
[10, 20, 15, 30, 40]

对应:

text
      10
     /  \
   20    15
  /  \
30   40

满足:

text
10 <= 20

10 <= 15

20 <= 30

20 <= 40

因此符合:

text
小顶堆

的性质。

再结合图来看:

image-20250305163533295

父节点和子节点之间的关系:

text
leftNo = parentNo * 2 + 1

rightNo = parentNo * 2 + 2

parentNo = (nodeNo - 1) / 2

通过这三个公式,就可以快速计算:

text
父节点下标

左子节点下标

右子节点下标

这也是为什么 PriorityQueue 可以直接使用数组保存堆结构。

4. 方法剖析 ​

4.1 add() 和 offer() ​

java
add(E e)

和:

java
offer(E e)

语义基本相同:

text
向优先队列中插入元素

Queue 接口中,两者对插入失败的处理方式不同:

text
add()
    ↓
失败时抛出异常


offer()
    ↓
失败时返回 false

对于 PriorityQueue 来说,这两个方法在正常添加元素时没有本质区别。

插入过程:

image-20250305163711388

新元素加入以后,可能会破坏:

text
堆结构

所以需要重新调整。

offer() 源码:

java
public boolean offer(
        E e) {

    // 不允许 null
    if (e == null) {

        throw new NullPointerException();
    }

    modCount++;

    int index =
            size;

    // 容量不足则扩容
    if (
            index
                    >= queue.length
    ) {

        grow(
                index + 1
        );
    }

    size =
            index + 1;

    // 第一个元素
    if (index == 0) {

        queue[0] =
                e;

    } else {

        // 向上调整
        siftUp(
                index,
                e
        );
    }

    return true;
}

其中:

java
grow()

和 ArrayList 中的扩容思路类似:

text
创建更大的数组
    ↓
复制原数组元素

这里真正重要的是:

java
siftUp()

它负责:

插入新元素以后,从当前位置不断向上调整,使堆重新满足优先级规则。

4.2 siftUp() ​

java
private void siftUp(
        int index,
        E element) {

    while (index > 0) {

        // parentIndex = (nodeIndex - 1) / 2
        int parent =
                (
                        index - 1
                ) >>> 1;

        Object parentElement =
                queue[parent];

        if (
                comparator.compare(
                        element,
                        (E) parentElement
                ) >= 0
        ) {

            break;
        }

        // 父节点下移
        queue[index] =
                parentElement;

        // 继续向上比较
        index =
                parent;
    }

    queue[index] =
            element;
}

整个调整过程:

text
新元素放在 index
    ↓
找到 parent
    ↓
新元素与 parent 比较

如果:

text
新元素优先级更高

就继续向上移动。

直到满足:

text
element >= parent

为止。

也就是:

从插入位置开始,让新元素逐层向上调整,直到重新满足堆结构。

这里的比较规则可以来自:

text
元素自然顺序

或者

Comparator

4.3 element() 和 peek() ​

java
element()

和:

java
peek()

都是:

text
获取但不删除队首元素

区别:

text
element()
    ↓
队列为空时抛出异常


peek()
    ↓
队列为空时返回 null

对于默认的小顶堆:

text
堆顶
    ↓
整个堆中的最小元素

堆使用数组保存,而堆顶对应:

text
queue[0]

所以直接获取:

java
queue[0]

即可。

示意图:

image-20250305163942359

源码:

java
public E peek() {

    if (size == 0) {

        return null;
    }

    return (E) queue[0];
}

核心就是:

text
queue[0]
    ↓
堆顶元素

4.4 remove() 和 poll() ​

java
remove()

和:

java
poll()

都是:

text
获取并删除队首元素

区别:

text
remove()
    ↓
失败时抛出异常


poll()
    ↓
失败时返回 null

删除堆顶元素以后:

text
堆结构发生变化

所以需要重新调整。

示意图:

image-20250305164040354

源码:

java
public E poll() {

    if (size == 0) {

        return null;
    }

    int lastIndex =
            --size;

    modCount++;

    // 保存原堆顶元素
    E result =
            (E) queue[0];

    // 获取最后一个元素
    E last =
            (E) queue[lastIndex];

    queue[lastIndex] =
            null;

    // 将最后一个元素放到堆顶后向下调整
    if (lastIndex != 0) {

        siftDown(
                0,
                last
        );
    }

    return result;
}

整个过程:

text
保存 queue[0]
    ↓
取出最后一个元素
    ↓
删除最后一个位置
    ↓
把最后一个元素放到堆顶
    ↓
siftDown()
    ↓
重新调整堆
    ↓
返回原来的堆顶元素

重点就是:

java
siftDown()

4.5 siftDown() ​

siftDown() 的作用:

从指定位置开始,让元素不断向下调整,与左右子节点中优先级更高的节点进行比较,直到重新满足堆结构。

源码:

java
private void siftDown(
        int index,
        E element) {

    int half =
            size >>> 1;

    while (
            index < half
    ) {

        // 左子节点
        int child =
                (
                        index << 1
                ) + 1;

        Object childElement =
                queue[child];

        // 右子节点
        int right =
                child + 1;

        // 找到左右子节点中优先级更高的那个
        if (
                right < size
                        && comparator.compare(
                        (E) childElement,
                        (E) queue[right]
                ) > 0
        ) {

            childElement =
                    queue[
                            child = right
                    ];
        }

        // 当前元素已经满足堆结构
        if (
                comparator.compare(
                        element,
                        (E) childElement
                ) <= 0
        ) {

            break;
        }

        // 子节点上移
        queue[index] =
                childElement;

        index =
                child;
    }

    queue[index] =
            element;
}

整个过程:

text
从 index 开始
    ↓
找到左右子节点
    ↓
选择其中优先级更高的一个
    ↓
与当前元素比较

如果当前元素已经满足:

text
堆结构

就停止。

否则:

text
子节点上移
    ↓
当前元素继续向下

直到找到合适的位置。

4.6 remove(Object o) ​

java
remove(Object o)

用于删除 PriorityQueue 中:

text
与 o 相等的某一个元素

如果存在多个相同元素:

text
只删除其中一个

这个方法并不是 Queue 接口独有的方法,而是来自:

java
Collection

删除任意位置的元素以后,同样可能破坏:

text
堆结构

所以需要调整。

主要分为两种情况:

text
1. 删除的是最后一个元素
    ↓
直接删除
    ↓
不需要调整


2. 删除的不是最后一个元素
    ↓
使用最后一个元素补到删除位置
    ↓
调用 siftDown()
    ↓
重新调整

示意图:

image-20250305164215819

源码:

java
public boolean remove(
        Object o) {

    // 找到第一个与 o 相等的元素
    int index =
            indexOf(o);

    if (index == -1) {

        return false;
    }

    int lastIndex =
            --size;

    // 删除的是最后一个元素
    if (
            lastIndex
                    == index
    ) {

        queue[index] =
                null;

    } else {

        // 保存最后一个元素
        E moved =
                (E) queue[lastIndex];

        queue[lastIndex] =
                null;

        // 从删除位置向下调整
        siftDown(
                index,
                moved
        );

        // ......
    }

    return true;
}

5. 小结 ​

PriorityQueue 是一种非常常用的数据结构,本质上是:

text
基于堆实现的优先队列

它的底层使用:

text
数组

保存堆中的元素,并通过堆的性质维护元素之间的优先级。

整体结构:

text
PriorityQueue
    ↓
数组
    ↓
完全二叉树
    ↓
堆

主要特点:

  • 底层使用数组保存元素,通过堆结构维护元素之间的优先级。
  • 取出元素时按照优先级顺序取出,可以按照从小到大,也可以通过比较规则改变顺序。
  • 元素之间的大小关系可以通过 Comparable 的自然顺序,或者构造时传入的 Comparator 决定。

插入元素:

text
offer()
    ↓
siftUp()
    ↓
向上调整

删除堆顶元素:

text
poll()
    ↓
最后一个元素移动到堆顶
    ↓
siftDown()
    ↓
向下调整

因此 PriorityQueue 能够在保持较高效率的同时,始终维护:

text
队首元素
    ↓
当前优先级最高的元素

Noctiflorous Garden · MIT License