今天,本人主要想复习优先队列这种数据结构。虽然标题号称“每日一省“,但是工作的人身不由己,已经很多天没有更新了。哈哈,废话不多说,直接进入正题吧。
1 优先队列的概念:
优先队列是一种抽象的数据结构,队列中的每个元素都有一个优先级值。优先级值用来表示该元素的出对的先后次序。
优先队列是至少允许插入操作和删除最小(或者最大)元素这两种操作的数据结构。通常,优先队列通过二叉堆来实现。
2 二叉堆的概念:
堆是一棵被完全填满的二叉树,底层例外,底层上的元素从左到右依次填入。如果使用数组表示二叉堆,那么对于数组上的任意位置i上的元素,其左子元素索引是2i,右子元素的索引是(2i +1),其父节点索引是[i/2]。
堆的性质,在堆中,每个元素都要保证大于等于另两个特定位置的元素。所以,在堆有序的二叉树中,每个节点又都小于等于它的父节点。所以,根节点是堆有序的二叉树中的最大节点。
当然,对的性质也可以反过来,也即:每个元素都要保证小于等于另两个特定位置的元素。所以,在堆有序的二叉树中,每个节点又都大于等于它的父节点。所以,根节点是堆有序的二叉树中的最小节点。
3 使用二叉堆实现的优先队列的代码示例
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class MaxFirstQueue<T> implements Iterable<T> {
private T[] queue; // 本列子中优先队列的底层实现是二叉堆,而二叉堆又用数组来表示
private int size = 0; // 表示队列中的元素个数,注意不是表示数组的容量
private Comparator<T> comparator; // 可选的比较器,可以作为该类的构造函数参数传入
public MaxFirstQueue(int max) {
queue = (T[]) new Comparable[max + 1];
}
public MaxFirstQueue() {
this(1);
}
public MaxFirstQueue(int initCapacity, Comparator<T> comparator) {
this.comparator = comparator;
queue = (T[]) new Object[initCapacity + 1];
size = 0;
}
public MaxFirstQueue(Comparator<T> comparator) {
this(1, comparator);
}
public MaxFirstQueue(T[] values) {
size = values.length;
queue = (T[]) new Object[values.length + 1];
for (int i = 0; i < size; i++) {
// 用数组表示二叉堆,数组索引为0的位置不存放元素,该位置元素值永远为空
queue[i + 1] = values[i];
}
for (int k = size / 2; k >= 1; k--) {
sinkDown(k);
}
assert isValidHeap();
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/*
* 判断数组queue中的所有元素构成的二叉堆是否满足二叉堆的基本性质,也即父节点元素最大, 然后每个子节点比其更下面的子节点大
*/
private boolean isValidHeap() {
return isValidSubHeap(1);
}
/*
* 判断以queue[k]位置为元素的子堆是否满足二叉堆的基本性质:也即父节点(父节点为queue[k])元素最大,
* 然后每个子节点比其更下面的子节点大
*/
private boolean isValidSubHeap(int k) {
if (k > size) {
return true;
}
int left = 2 * k, right = 2 * k + 1;
if (left <= size && less(k, left)) {
return false;
}
if (right <= size && less(k, right)) {
return false;
}
return isValidSubHeap(left) && isValidSubHeap(right);
}
/**
* 取出最大值
*/
public T max() {
if (isEmpty()) {
throw new NoSuchElementException("每次可以取出最大元素的优先队列已经为空,没有任何元素存储其中了");
}
return queue[1];
}
/**
* 插入元素
*/
public void add(T value) {
if (size >= queue.length - 1) {
resize(2 * queue.length);
}
queue[++size] = value;
swimUP(size);
assert isValidHeap();
}
/**
* 删除一个元素后,实际是把删除的元素的放到了索引为size的位置(再把该位置元素值设为null), 该位置已经不能算做是二叉堆的一部分了。
* 二叉堆的大小变为size--
*
* 下面的三行代码可以合写为exchange(1, size--), queue[size + 1] = null,这三行代码如下:
* exchange(1, size); queue[size] = null; size--;
*/
public T deleteMax() {
if (isEmpty()) {
throw new NoSuchElementException("每次可以取出最大元素的优先队列已经为空,没有任何元素存储其中了");
}
T value = queue[1];
exchange(1, size);
queue[size] = null;
size--;
sinkDown(1);
if ((size > 0) && (size == (queue.length - 1) / 4)) {
resize(queue.length / 2);
}
assert isValidHeap();
return value;
}
/*
* 调整底层数组的大小:具体做法是创建指定大小的临时数组,然后把queue中的元素移动到这个临时数组的相应位置,
* 最后再将这个临时数组设置为优先队列的底层实现。
*/
private void resize(int capacity) {
assert capacity > size;
T[] temp = (T[]) new Object[capacity];
for (int i = 1; i <= size; i++) {
temp[i] = queue[i];
}
queue = temp;
}
private boolean less(int i, int j) {
if (comparator == null) {
return ((Comparable<T>) queue[i]).compareTo(queue[j]) < 0;
} else {
return comparator.compare(queue[i], queue[j]) < 0;
}
}
private void exchange(int i, int j) {
T temp = queue[i];
queue[i] = queue[j];
queue[j] = temp;
}
/*
* 将元素值大于父节点的子节点与父节点交换位置,保持堆有序。交换位置后,原来的子节点可能仍然比 更上层的父节点大,
* 所以整个过程需要递归进行。这样一来,原来的子节点 有可能升级为层级更高的父节点,类似于一个物体从湖底往上浮直到达到其重力与浮力相平衡的过程。
*/
private void swimUP(int k) {
while (k > 1 && less(k / 2, k)) {
exchange(k, k / 2);
k = k / 2;
}
}
/*
* 将元素值小于子节点的父节点与子节点交换位置,交换位置后, 原来的父节点仍然有可能比其下面的子节点小,
* 所以还需要继续进行类相同的操作,以便保持堆的有序性。所以整个过程递归进行。
* 这类似于一个物体从湖面下沉到距离湖底的某个位置,直到达到其重力与浮力相平衡为止。
*/
private void sinkDown(int k) {
while (2 * k <= size) {
int j = 2 * k;
if (j < size && less(j, j + 1)) {
j++;
}
if (!less(k, j)) {
break;
}
exchange(k, j);
k = j;
}
}
@Override
public Iterator<T> iterator() {
return new MaxFirstQueueIterator();
}
/**
* 用于遍历优先队列中元素的迭代器的具体实现:
*/
private class MaxFirstQueueIterator implements Iterator<T> {
/*
* 相当于被迭代的优先队列的一个副本,遍历优先队列中的元素其实遍历的是其副本,
* 所以,在多线程环境下,并发的增加、删除、遍历元素可能会导致数据错乱。
*/
private MaxFirstQueue<T> copy;
public MaxFirstQueueIterator() {
if (comparator == null) {
copy = new MaxFirstQueue<T>(size());
} else {
copy = new MaxFirstQueue<T>(size(), comparator);
}
for (int i = 1; i <= size; i++)
copy.add(queue[i]);
}
public boolean hasNext() {
return !copy.isEmpty();
}
public void remove() {
throw new UnsupportedOperationException("迭代器不支持移除操作");
}
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return copy.deleteMax();
}
}
/**
* 主方法:包含有测试代码
*/
public static void main(String[] args) {
MaxFirstQueue<String> pq = new MaxFirstQueue<String>();
pq.add("S");
pq.add("T");
System.out.println("====================================================");
for (String e : pq) {
System.out.println(e);
}
pq.add("C");
pq.add("A");
pq.add("M");
pq.add("B");
System.out.println("====================================================");
for (String e : pq) {
System.out.println(e);
}
System.out.println("====================================================");
Iterator<String> iterator = pq.iterator();
iterator.forEachRemaining(t -> System.out.print("-" + t + "-"));
System.out.println();
System.out.println("====================================================");
try {
System.out.println(pq.deleteMax());
System.out.println(pq.deleteMax());
System.out.println(pq.deleteMax());
System.out.println(pq.deleteMax());
System.out.println(pq.deleteMax());
System.out.println(pq.deleteMax());
System.out.println(pq.deleteMax());
} catch (Exception e) {
System.out.println("队列为空,大小为" + pq.size);
}
pq.add("C");
pq.add("A");
pq.add("M");
pq.add("B");
System.out.println("队列大小为" + pq.size);
}
}