Java中提供了优先队列的实现 — PriorityQueue,其底层实现的数据结构为heap(堆)。

关于堆

堆的性质

  1. 堆中某个节点的值总是不大于或不小于其父节点的值。
  2. 堆总是一棵完全二叉树。

时间复杂度

我们常常会使用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方法的时候是有序的。但当我们直接遍历整个容器的时候,我们就会发现,存储时并不是有序的。