知乎:zhuanlan.zhihu.com/p/78071908
概念
PriorityQueue 是一个优先队列,每次出队的元素是优先级最高的。
示例
public class Demo {
public static void main(String args[]) {
java.util.PriorityQueue<Integer> queue = new java.util.PriorityQueue<>();
queue.add(100);
queue.add(50);
queue.add(2);
queue.add(31);
queue.add(5);
System.out.println("初始队列: " + queue);
System.out.println("出队: " + queue.poll());
System.out.println("当前队列: " + queue);
System.out.println("出队: " + queue.poll());
System.out.println("当前队列: " + queue);
}
}
初始队列: [2, 5, 50, 100, 31]
出队: 2
当前队列: [5, 31, 50, 100]
出队: 5
当前队列: [31, 100, 50]- 队列中的元素并不是完全有序的
- 队列的头节点在数值上是最小的
DIY
自己动手实现一个简单的优先队列
设计思路
- 队列底层存储可以选择使用数组或链表,但是由于数组的增删逻辑比较复杂,所以优先使用链表来实现。
- 每当插入一个元素,我们就遍历底层的链表,逐一比较大小。确保新元素插入后仍然保持整体有序。
- 使链表的头节点优先度最高,那么就能保证出队的元素优先级最高。
代码实现
import java.util.Comparator;
import java.lang.Comparable;
public class DiyPriorityQueue<E extends Comparable> {
private Node head;
static class Node<E> {
public E value;
public Node<E> next;
public Node<E> pre;
}
public void add(E e) {
if (head == null) {
Node<E> node = new Node<E>();
node.value = e;
head = node;
return;
}
Node<E> n = head;
while(n != null) {
if (n.value.compareTo(e) < 0) {
if (n.pre == null) {
Node<E> eNode = new Node<>();
eNode.next = n;
eNode.value = e;
head = eNode;
n.pre = eNode;
break;
} else {
Node<E> eNode = new Node<>();
eNode.next = n;
eNode.value = e;
eNode.pre = n.pre;
n.pre.next = eNode;
n.pre = eNode;
break;
}
} else if (n.value.compareTo(e) > 0 && n.next == null) {
Node<E> eNode = new Node<>();
n.next = eNode;
eNode.value = e;
eNode.pre = n;
break;
}
n = n.next;
}
}
public E poll() {
if (head == null) {
return null;
}
Node<E> result = head;
head = head.next;
return result.value;
}
public String toString() {
if (head == null) return "";
String result = head.value.toString();
Node e = head;
while((e = e.next) != null) {
result = result + "," + e.value.toString();
}
return result;
}
public static void main(String args[]) {
DiyPriorityQueue<Integer> queue = new DiyPriorityQueue<>();
queue.add(1);
queue.add(2);
queue.add(0);
queue.add(100);
queue.add(98);
System.out.println(queue);
// 100, 98, 2, 1, 0
System.out.println(queue.poll()); // 100
System.out.println(queue.poll()); // 98
System.out.println(queue.poll()); // 2
System.out.println(queue.poll()); // 1
System.out.println(queue.poll()); // 0
System.out.println(queue.poll()); // null
}
}缺陷
- 每次入队时,对需要逐个比较,时间复杂度较高
延伸
- 队列底层如果使用数组来实现,那么插入可以使用二分查找法提高查找插入位置的效率。但是当数组插入元素时,就会引起剩余元素的迁移问题,引起大量的数据拷贝。同时,如果底层数组容量过小,则又会触发扩容拷贝的现象。但是 JDK 的优先队列底层确是使用数组来存储的,后面我们会看到具体的实现。
JDK8
设计思路
- 使用二叉堆(最小堆)作为底层数据存储结构
- 使用数组实现二叉堆
二叉堆(Binary Heap)
性质
- 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
- 二叉堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
下图为最小堆示例。
存储
我们可以使用数组来存储上面的这个二叉堆。并且元素的索引有如下特性:
- 左叶子节点索引 = 其父节点索引 * 2 + 1; left = parent * 2 + 1
- 右叶子节点索引 = 其父节点索引 * 2 + 2; right = parent * 2 + 2
- 某父节点索引 = (其任一子节点索引 - 1) / 2; parent = (child - 1) / 2
代码实现
主要成员变量
public class PriorityQueue<E> {
/**
* Priority queue represented as a balanced binary heap: the two
* children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
* priority queue is ordered by comparator, or by the elements'
* natural ordering, if comparator is null: For each node n in the
* heap and each descendant d of n, n <= d. The element with the
* lowest value is in queue[0], assuming the queue is nonempty.
*/
Object[] queue;
int size = 0;
}添加元素 add(E e) offer(E e)
将待插入元素从队列末尾逐个比较,并进行替换。过程图如下。
待插入元素值为1,将其放在二叉堆的最后一个位置
找到对应的父节点,发现 1 < 15,则将 1 和 15 进行对调
继续寻找对应的父节点,发现是根节点 5,而 1 < 5,所以仍然执行对调操作
对调操作全部完成,此时二叉堆的根为最小值 1
Java 具体 add、offer 代码
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
// ...
int i = size;
// 如果元素数量大于等于队列长度,则做扩容处理
// 数组拷贝
if (i >= queue.length)
grow(i + 1);
size = i + 1;
// 如果 queue 为空,那么当前元素直接放在数组首位
if (i == 0)
queue[0] = e;
else
// 尝试从数组最后节点开始上移
siftUp(i, e);
return true;
}
siftUp 判断是否使用自定义比较器
private void siftUp(int k, E x) {
if (comparator != null)
// 基于自定义比较器比较队列元素
siftUpUsingComparator(k, x);
else
// 使用类型强转,将队列元素转换为可比较类型
siftUpComparable(k, x);
}
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
// k 为指针,从数组最后一个元素反向遍历
while (k > 0) {
// parent = (current - 1) / 2
int parent = (k - 1) >>> 1;
Object e = queue[parent];
// 比较待添加节点与当前节点,如果待添加节点比当前节点大或者相等,则跳出
if (key.compareTo((E) e) >= 0)
break;
// 如果待添加节点小于当前节点,那么需要执行交换逻辑
// 当前节点使用父节点赋值
queue[k] = e;
// 指针指向父节点
k = parent;
}
// 目标 k 位置值更新为待插入值
queue[k] = key;
}取出根元素 poll()
将根元素移除
将最后一个元素放置根节点
红色节点子节点中最小的为橙色节点
红色节点大于橙色节点,执行交换
红色节点的子节点都比红色节点大,则操作完毕
Java 具体 poll 代码
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
// result 就是要 poll 出来的就是堆顶
E result = (E) queue[0];
// s 为数组最后一个元素的指针
// x 则指向最后一个元素
E x = (E) queue[s];
// 将最后一个元素置为 null
queue[s] = null;
if (s != 0)
// 目前堆顶的元素是原先数组的最后一个元素
// 不符合堆序性这个特性,所以需要进行调整
siftDown(0, x);
return result;
}
private void siftDown(int k, E x) {
if (comparator != null)
// 如果有自定义比较器,则使用自定义比较器
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
// 这儿取一半还没搞懂
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
// 先将指针指向左叶子节点
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
// 如果右叶子节点比左叶子节点小
// 则将 c 赋值为更小的那个叶子节点
c = queue[child = right];
// 现在将 x 与更小的叶子节点做比较
// 如果 x 小于等于叶子节点,那说明堆序性已保持,则跳出循环
if (key.compareTo((E) c) <= 0)
break;
// x 大于叶子节点
// 将当前 k 位置上的元素赋值为叶子节点中更小的节点
queue[k] = c;
// 遍历指针指向叶子节点中更小的节点
k = child;
}
queue[k] = key;
}
判断元素是否存在 contains(Object o)
public boolean contains(Object o) {
return indexOf(o) != -1;
}
// 堆的性质无法通过二分查找进行遍历
// 所以直接遍历底层数组依次判断是否 equal 即可
private int indexOf(Object o) {
if (o != null) {
// size 为堆元素数量
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
删除指定元素 remove(Object o)
public boolean remove(Object o) {
// indexOf 即上面提到过,复杂度为 O(n)
int i = indexOf(o);
if (i == -1)
return false;
else {
// 如果找到了元素,则执行删除操作
removeAt(i);
return true;
}
}
private E removeAt(int i) {
// ...
// s 表示元素被删除后,当前队列的元素数量
int s = --size;
// 如果删除的是最后一个元素,那么直接将数组有效元素的最后一位置为 null 即可。
// 等价于 if ((size - 1) == i) { ... }
if (s == i)
queue[i] = null;
else {
// 把最后一个有效元素标记为待移动的元素
E moved = (E) queue[s];
// 最后一个有效元素置为 null
queue[s] = null;
// 将最后一个元素从当前被删除位置开始向下置换
siftDown(i, moved);
// 如果发现待移动的元素没有移动
if (queue[i] == moved) {
// 尝试将其向上置换
siftUp(i, moved);
// 如果发现待移动的元素移动了,那就作为返回值
if (queue[i] != moved)
// 这儿返回的是队列中有效元素的最后一个元素
return moved;
}
}
return null;
}