PriorityQueue 优先队列,它的数据结构是数组实现的队列,但体现形式是一棵二叉堆树结构。在元素存放时,通过对存放元素的比较和替换形成二叉堆结构。
二叉堆是一种特殊结构的堆,它的表现形态可以是一棵完整或近似二叉树的结构。例如,我们在本章节中实现的延迟队列中的元素存放,使用的就是 `PriorityQueue` 实现的平衡二叉堆结构。数据以队列形式存放在基础数组中。
父子节点索引关系
父节点:假设父节点为 `queue[n]`,那么:
左子节点为 `queue[2n + 1]`
右子节点为 `queue[2n + 2]`
父节点位置:任意孩子节点的父节点位置为 `(n - 1) >>> 1`,相当于减1后除2取整。
节点间大小关系
父节点:父节点的值小于或等于任意孩子节点的值。
同一层级的两个孩子节点:同一层级的两个孩子节点之间的大小关系不需要维护,它是在弹出元素时进行判断的。
子叶节点与非子叶节点
在一个长度为 `size` 的优先级队列中:
当 `index >= size >>> 1` 时,该节点为叶子节点。
否则,该节点为非叶子节点。
public class MyPriorityQueue<E> implements MyQueue<E> {
// 默认容量,初始数组大小为10
private static final int DEFAULT_CAPACITY = 10;
// 队列中元素的数量
private transient int size = 0;
// 存储元素的数组,使用Object数组以支持泛型
private transient Object[] elements;
private transient Comparable<? super E> key;
/**
* 构造函数,初始化elements数组,默认容量为10。
*/
public MyPriorityQueue() {
elements = new Object[DEFAULT_CAPACITY];
}
/**
* 返回队列中元素的数量。
*
* @return 队列中元素的数量
*/
@Override
public int size() {
return size;
}
/**
* 判断队列是否为空。
*
* @return 如果队列为空返回true,否则返回false
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* 添加元素到队列,等同于offer方法。
*
* @param e 要添加的元素
* @return 如果添加成功返回true
* @throws NullPointerException 如果元素为null
*/
@Override
public boolean add(E e) {
return offer(e);
}
/**
* 添加元素到队列,并返回是否添加成功。
*
* @param e 要添加的元素
* @return 如果添加成功返回true
* @throws NullPointerException 如果元素为null
*/
@Override
public boolean offer(E e) {
// 检查元素是否为空
if (e == null) {
throw new NullPointerException();
}
// 如果当前容量足够,直接添加并调整堆
if (size < elements.length) {
elements[size++] = e;
siftUpComparable(size - 1, e);
return true;
} else {
// 容量不足时,扩容再添加
grow(size + 1);
elements[size++] = e;
siftUpComparable(size - 1, e);
return true;
}
}
/**
* 移除并返回队列头部的元素,队列不能为空。
*
* @return 队列头部的元素
* @throws NoSuchElementException 如果队列为空
*/
@Override
public E remove() {
if (size > 0) {
E e = (E) elements[0];
elements[0] = elements[--size];
siftDownComparable(0, e);
return e;
}
return null;
}
/**
* 移除并返回队列头部的元素,队列为空时返回null。
*
* @return 队列头部的元素,如果队列为空返回null
*/
@Override
public E poll() {
if (size > 0) {
E e = (E) elements[0];
int len = --size;
E x = (E) elements[len];
elements[len] = null;
if (len > 0) {
siftDownComparable(0, x);
}
return e;
}
return null;
}
/**
* 查看队列头部的元素,不移除。
*
* @return 队列头部的元素,如果队列为空返回null
*/
@Override
public E peek() {
return size > 0 ? (E) elements[0] : null;
}
/**
* 打印当前队列的元素。
*/
@Override
public void printQueue() {
System.out.println("当前队列:" + Arrays.toString(elements));
}
/**
* 扩容方法,当元素数量超过当前数组容量时调用。
*
* @param minCapacity 最小容量
*/
private void grow(int minCapacity) {
int oldCapacity = elements.length;
// 计算新容量,如果旧容量小于64,则增加2,否则增加旧容量的一半
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
// 检查新容量是否超过Integer.MAX_VALUE - 8
if (newCapacity - (Integer.MAX_VALUE - 8) > 0) {
newCapacity = (minCapacity > (Integer.MAX_VALUE - 8) ? Integer.MAX_VALUE : (Integer.MAX_VALUE - 8));
}
// 复制数组到新的容量
elements = Arrays.copyOf(elements, newCapacity);
}
/**
* 将新添加的元素向上调整,以维持堆的性质。
*
* @param k 元素的索引
* @param e 要调整的元素
*/
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E e) {
key = (Comparable<? super E>) e;
while (k > 0) {
// 获取父节点index,相当于除以2, 相当于减1后除2取整
int parent = (k - 1) >>> 1;
System.out.println("入队——当前位置:" + k + ", 父节点位置:" + parent);
Object o = elements[parent];
// 如果当前元素不小于父元素,则位置合适,退出循环
if (key.compareTo((E) o) >= 0) {
System.out.println("入队——位置合适,退出循环");
break;
}
// 否则,将父元素下移
System.out.println("入队——位置不合适,将父节点下移");
elements[k] = o;
k = parent;
}
// 将新元素放在正确的位置
elements[k] = key;
System.out.println("调整后队列:" + Arrays.toString(elements));
}
/**
* 将元素向下调整,以维持堆的性质。
*
* @param k 元素的索引
* @param e 要调整的元素
*/
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E e) {
key = (Comparable<? super E>) e;
int half = size >>> 1;
while (k < half) {
// 找到左子节点和右子节点,两个节点进行比较,找出最大的值
int child = (k << 1) + 1;
Object c = elements[child];
int right = child + 1;
// 如果右子节点存在且小于左子节点,则选择右子节点
if (right < size && ((Comparable<? super E>) c).compareTo((E) elements[right]) > 0) {
System.out.println("出队——左右对比取小" + "left:" + c + "right:" + elements[right]);
c = elements[child = right];
}
// 如果当前元素不大于子节点,则位置合适,退出循环
if (key.compareTo((E) c) <= 0) {
break;
}
// 否则,将子节点上移
System.out.println("出队——上下对比" + "left:" + elements[k] + "right:" + c);
elements[k] = c;
k = child;
}
System.out.println("出队——最终位置" + "left:" + elements[k] + "right:" + e);
// 将原元素放在正确的位置
elements[k] = key;
}
}
测试:
public class Test {
public static void main(String[] args) {
MyPriorityQueue<Integer> queue = new MyPriorityQueue<>();
queue.offer(1);
queue.offer(3);
queue.offer(5);
queue.offer(11);
queue.offer(4);
queue.offer(6);
queue.offer(7);
queue.offer(2);
queue.printQueue();
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
测试结果:
调整后队列:[1, null, null, null, null, null, null, null, null, null]
入队——当前位置:1, 父节点位置:0
入队——位置合适,退出循环
调整后队列:[1, 3, null, null, null, null, null, null, null, null]
入队——当前位置:2, 父节点位置:0
入队——位置合适,退出循环
调整后队列:[1, 3, 5, null, null, null, null, null, null, null]
入队——当前位置:3, 父节点位置:1
入队——位置合适,退出循环
调整后队列:[1, 3, 5, 11, null, null, null, null, null, null]
入队——当前位置:4, 父节点位置:1
入队——位置合适,退出循环
调整后队列:[1, 3, 5, 11, 4, null, null, null, null, null]
入队——当前位置:5, 父节点位置:2
入队——位置合适,退出循环
调整后队列:[1, 3, 5, 11, 4, 6, null, null, null, null]
入队——当前位置:6, 父节点位置:2
入队——位置合适,退出循环
调整后队列:[1, 3, 5, 11, 4, 6, 7, null, null, null]
入队——当前位置:7, 父节点位置:3
入队——位置不合适,将父节点下移
入队——当前位置:3, 父节点位置:1
入队——位置不合适,将父节点下移
入队——当前位置:1, 父节点位置:0
入队——位置合适,退出循环
调整后队列:[1, 2, 5, 3, 4, 6, 7, 11, null, null]
当前队列:[1, 2, 5, 3, 4, 6, 7, 11, null, null]
出队——上下对比left:1right:2
出队——上下对比left:2right:3
出队——最终位置left:3right:11
1
出队——上下对比left:2right:3
出队——左右对比取小left:11right:4
出队——上下对比left:3right:4
出队——最终位置left:4right:7
2
出队——上下对比left:3right:4
出队——左右对比取小left:11right:7
出队——最终位置left:4right:6
3
出队——左右对比取小left:6right:5
出队——上下对比left:4right:5
出队——最终位置left:5right:7
4
出队——上下对比left:5right:6
出队——最终位置left:6right:11
5
出队——最终位置left:6right:7
6
出队——最终位置left:7right:11
7
11