Java中提供了优先队列的实现 — PriorityQueue,其底层实现的数据结构为heap(堆)。
关于堆
堆的性质
- 堆中某个节点的值总是不大于或不小于其父节点的值。
- 堆总是一棵完全二叉树。
时间复杂度
我们常常会使用PriorityQueue去实现大顶堆(堆顶是最大的元素)或者小顶堆(堆顶是最小的元素),其内部的存储结构只是普通的数组,如果每次都进行一次快排的话复杂度可想而知。而堆排序的时间复杂度是O(nlogn),因为初始建堆的时间复杂度是O(n),而后续插入或删除元素的时候复杂度是O(nlogn)。
在使用PriorityQueue的时候,我们可以逐个元素插入或者把实现了Collection接口的集合传入,但无论如何我们总的时间复杂度都是O(nlogn),因为一个个元素插入的时候,需要遍历元素O(n) 以及调整堆O(logn),而放入集合的时候则需要初始化堆。
offer与poll的流程
offer方法用于插入元素,而poll用于把堆顶元素弹出。
插入元素
在插入元素的时候,会把元素放到堆底,然后依次与父元素比较,如果大于(或小于)父元素,则交换位置,否则直接插入到当前位置。
public boolean offer(E e) {
// null检测
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;
}
// k = size x = 待插入元素
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;
}
删除元素
在进行poll操作时,会把堆顶元素弹出,然后把堆底的元素放到堆顶,然后进行调整。
public E poll() {
// empty
if (size == 0)
return null;
int s = --size;
// 迭代器并发计数
modCount++;
// 获取堆顶元素
E result = (E) queue[0];
// 获取堆底元素
E x = (E) queue[s];
// 方便gc
queue[s] = null;
// 进行堆调整
if (s != 0)
siftDown(0, x);
return result;
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
// 当不是叶子节点的时候需要一直调整
int half = size >>> 1;
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 = queue[child = right];
// 当前元素与较小(大)的元素比较,如果大于(小于)子元素,则不需要继续调整
if (key.compareTo((E) c) <= 0)
break;
// 与子节点交换位置,并从子节点位置继续调整
queue[k] = c;
k = child;
}
// 把元素放入特定位置
queue[k] = key;
}
优先队列到底是否有序?
优先队列在存储是并非有序。因为堆的性质1:堆中某个节点的值总是不大于或不小于其父节点的值。因此我们无法知道一个节点的两个子节点的大小关系。做个小实验验证一下。
public void test() {
PriorityQueue<Integer> queue = new PriorityQueue<>(Arrays.asList(5,6,4,8,1));
for (Integer value : queue) {
System.out.print(value + " ");
}
System.out.println("\n==================");
int size = queue.size();
for (int i = 0; i < size; i++) {
System.out.print(queue.poll() + " ");
}
}
由于每次插入或删除元素时都会对堆进行调整,所以调用poll方法的时候是有序的。但当我们直接遍历整个容器的时候,我们就会发现,存储时并不是有序的。