目录
普通的队列是先进先出的数据结构,而优先队列为元素赋予优先级,具有最高优先级的元素成为队列首部。
优先队列一般基于二叉堆实现。
本文会分析java中几种常见的优先队列:PriorityQueue
、PriorityBlockingQueue
、DelayQueue
、DelayedWorkQueue
。
一、二叉堆的基本原理
(一) 什么是二叉堆?
- 完全二叉树
- 堆的根节点的优先级最大(即最大或最小)
- 父节点的优先级必定大于子节点,兄弟节点的优先级不确定谁大谁小
时间复杂度 | |
---|---|
插入 | O(log n) |
删除 | O(log n) |
构造 | O(n) |
(二) 堆的用途
- 取最值
(三) 堆的基本操作
1. 插入
往堆插入元素,基本思想是从最后一个位置开始,通过上浮操作不断调整位置,直到满足父节点的优先级必定大于子节点这个条件。
上浮
上浮是往二叉堆添加元素用到的操作,它其实是不断的调整k的位置为父元素的位置直到满足条件为止。
// 用数组表示堆
Object []objs = new Object[10];
/**
* 上浮:
* k表示堆的最后一个位置;
* obj表示将要插入的元素。
*/
private void siftUp(int k, Object obj) {
// 1. 判断k是否为根元素的位置0,如果是则直接赋值
while(k>0) {
// 2. 获取父元素的位置,parent = (k-1)/2
int parent = (k-1) >>> 1;
// 3. 如果父元素的优先级大于等于obj,跳出循环并插入obj
if(objs[parent] >= obj) {
break;
}
// 4. 如果父元素的优先级小于obj,将父元素赋值到k的位置,更改k为父元素的位置,继续循环
objs[k] = objs[parent];
k = parent;
}
// 5. 为obj赋值
objs[k] = obj;
}
/**
* 添加元素,不考虑数组扩容的情况。
* 假设size表示当前堆包含的元素个数(注意不一定等于上面定义的10)
*/
public void add(Object obj) {
if(size==0) {
objs[0] = obj;
} else {
siftUp(size, obj);
size++;
}
}
2. 删除
删除指定位置的元素,其基本思想是从指定位置开始,把最后一个元素放到被删除元素的位置,通过下沉或者上浮操作,使得堆满足父元素优先级大于子元素的条件。
下沉
下沉是删除时用到的操作。它是把最后一个元素放到被删除元素的位置,然后重新调整使得堆满足条件的过程。
- 当被删除元素的位置位于最后一个元素的父元素的位置后面时,可以直接把最后一个元素插入到被删除元素的位置;然后再进行上浮操作。
- 否则,需要执行下沉操作。
// 用数组表示堆
Object []objs = new Object[10];
/**
* k被删除元素的位置;
* obj堆的最后一个元素;
* 假设size为当前堆包含元素的个数(不一定是上面定义的10)
*/
private void siftDown(int k, Object obj) {
// 1. 找到最后一个元素的父节点的位置, (最后一个元素位置-1) / 2
int parent = (size-1-1) >>> 1;
// 2. 判断k是否在父节点位置之后,如果在之前则需要下沉操作
while(k <= parent) {
// 3.获取k的左右子节点的位置
int left = k<<<2 +1;
int right = left+1;
// 4.选择左右子节点中优先级最高的一个
int best;
if (objs[left] > objs[right]) {
best = left;
} else {
best = right;
}
// 5.判断obj和best的优先级谁高。如果obj优先级高,则跳出循环直接赋值,否则继续下沉
if (obj >= objs[best]) {
break;
}
objs[k] = objs[best];
k = best;
}
// 6.赋值
objs[k] = obj;
}
/**
* 删除第p个元素。
*/
public void remove(int p) {
// 1.获取最后一个元素
Object obj = objs[size-1];
// 2.如果p不等于最后一个元素
if (p != size-1) {
// 3.把最后一个元素和p进行下沉操作
siftDown(p, obj);
if(objs[p] == obj) {
// 4. 上浮
siftUp(p, obj);
}
}
size--;
}
二、PriorityQueue
(一) PriorityQueue是什么?
PriorityQueue
是基于二叉堆原理的优先队列,队列用动态数组实现。- 它是非阻塞的、非线程安全的;
PriorityQueue
内部维护了几个重要属性:
类型 | 含义 | |
---|---|---|
queue | Object[] | 代表PriorityQueue 的队列,存放元素 |
size | int | 当前队列包含元素个数 |
comparator | Comparator | PriorityQueue 根据比较器对元素优先级排序 |
modCount | int | 记录队列被修改的次数 |
(二) PriorityQueue的使用
PriorityQueue
的使用非常简单,一笔带过。
public static void main(String[] args) {
PriorityQueue<String> queue = new PriorityQueue<>();
queue.add("hello");
System.out.println(queue.peek());
queue.remove("hello");
System.out.println(queue.peek());
}
(三) PriorityQueue的实现原理
PriorityQueue
的基本原理和第一部分讲的基本一致。
插入
- 如果当前元素个数达到数组长度,则扩容;
1.1 如果数组长度小于64,直接扩容1倍
1.2 否则扩容0.5倍 - 增加元素个数
- 上浮操作,找到合适的位置插入。在上浮的时候,如果comparator存在则用它确定优先级,否则用自然顺序确定优先级。
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;
}
删除
- 获取最后一个元素
- 将待删除的位置i和最后一个元素做下沉操作
- 如果没有执行下沉操作,那么表示待删除的位置i是叶节点,需要通过上浮调整二叉堆
- 执行上浮操作
private E removeAt(int i) {
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;
}
三、PriorityBlockingQueue
(一) PriorityBlockingQueue是什么?
- 是
PriorityQueue
线程安全版本 - 同样是基于二叉堆的原理,用动态数组实现
- 阻塞的、线程安全的
(二) PriorityBlockingQueue的实现原理
插入
原理基本和PriorityQueue
一样
- 加锁
- 是否需要扩容,扩容原理和
PriorityQueue
一样 - 上浮
- 解锁
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
删除
基本原理和PriorityQueue
一致,只是使用ReentrantLock
保证线程安全。
四、DelayQueue
- 基于
PriorityQueue
实现的延迟队列 - 它的删除、插入操作和
PriorityQueue
基本一致,主要的区别在与poll()
、take()
等方法。PriorityQueue
是只要队列首部有数据就除移,而DelayQueue
还需要判断是否达到除移的时间。 - 至于
take()
、poll()
方法的区别在于,take()
会阻塞,而poll()
直接返回。
五、DelayedWorkQueue
原理与DelayQueue相同,不知道JDK开发团队为何重复造一个轮子。
六、一图胜千言
添加
删除
七、F&Q
1、侄节点与叔节点的大小关系?
没有规定两种的大小关系,叔节点的优先级可以比侄节点大也可以小。