文章目录
- 1 概述
- 1.1 需求
- 1.2 优先队列特点
- 1.3 优先队列分类
- 1.4 应用场景
- 1.5 相关延伸
- 2 说明
- 3 索引优先队列
- 3.1 实现思路
- 3.2 API设计
- 3.2 代码实现及简单测试
- 5 主要方法讲解
- 5.1 exch()
- 5.2 insert()
- 5.2 poll()
- 6 分析
- 7 后记
1 概述
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用堆数据结构来实现。
1.1 需求
许多应用程序都需要处理有序的元素,但不一定要求它们全部有序,或是不一定要一次就将它们排序。很多情况下我们会收集一些元素,处理当前键最大(或最小)的元素,然后在收集更多的元素,在处理当前键值最大(或最小)的元素,如此这般。例如,在一台同时运行多个应用程序的电脑(或手机),它通常为每个应用程序分配一个优先级,并总是处理下一个优先级最高的事件。比如大多数手机分配给来电的优先级都会比游戏程序高。
1.2 优先队列特点
- 支持两种基本操作:
- 删除最大(或最小)元素
- 插入元素
- 使用和队列以及栈类似,但是处理插入和删除更高效
1.3 优先队列分类
-
按队列删除元素是最大还是最小
- 最大优先队列:删除的元素为最大值
- 最小优先队列:删除的元素为最小值
-
按内部实现:
-
基于数组的优先队列
- 无序
- 有序
-
基于链表优先队列
- 无序
- 有序
-
堆(二叉堆)
-
索引优先队列
-
1.4 应用场景
一些常见常见如下:
- 模拟系统:其中事件的键即为发生的时间,而系统需要按照时间顺序处理所有事件。
- 任务调度:其中键值对应的优先级决定了应该首先执行那些任务
- 数值计算:键值代表计算错误,而我们需要按照键值指定的顺序来修正它们。
1.5 相关延伸
我们可以使用优先队列实现排序算法,一种名为堆排序的重要排序算法来自于基于堆的优先队列实现。
2 说明
基于数组和链表的普通优先队列实现,因为它们的插入和删除操作其中之一在最坏情况下需要线性时间来完成,应用中不会使用,我们不实现,如果有兴趣可以自己实现或查阅相关文档。关于堆和堆排序,我们在堆(二叉堆)-优先队列-数据结构和算法(Java)和堆排序-排序-数据结构和算法中已经解决,下面我们学习下索引优先队列。
优先队列的各种实现在最坏情况下运行时间的增长量级如下表2-1所示:
数据结构 | 插入元素 | 删除最大(或最小)元素 |
---|---|---|
有序数组(或链表) | N | 1 |
无序数组(或链表) | 1 | N |
堆 | log N \log N logN | log N \log N logN |
理想情况 | 1 | 1 |
使用无序序列解决问题是惰性方法,我们仅在必要的时候才会采取行动(找出最大或最小元素);使用有序序列是解决问题的积极方法,因为我们会尽可能的未雨绸缪(在插入元素时就保持列表有序),是后续操作更高效。
3 索引优先队列
在很多应用中,允许用例引用已经进入优先队列中的元素是有必要的。做到这一点的一种简单方法就是给每个元素一个索引,也就是我们下面要学习的索引优先队列。我们以最小索引优先队列为例,进行讲解。
3.1 实现思路
-
首先有一个数组table存放E类型的数据,插入元素e的时候同时指定一个索引i,即放入数组table的索i处。
-
然后准备一个堆排序数组pq,来存放元素对应的索引i,此时通过table[pq[i]]很容易找到元素,如下图所示:
-
新元素插入或者删除元素或者改变元素,破坏了堆有序,此时我们需要进行调整,我们对pq进行堆排序,而不是对table数组排序,此时堆有序如下图所示:
- 此堆有序从元素插入开始排序,不是之后的堆排序调整,可以使用之前实现的二叉堆校验。
-
当进行堆排序之后,我们怎么通过原先的索引i快速定位到元素呢?一种方式是遍历pq数组,但是效率太慢,这时我们在引入第三个数组qp为数组pq的逆数组,这样呢qp索引就与table索引一致。通过table[qp[pq[i]]]很容易定位到我们改动的元素。
- 逆数组即数组pq的值为qp的索引,而pq的索引为qp的值。
-
三个数组最终内存示意图如下:
-
详细的插入、删除等方法实现,我们在代码实现部分给出分析。
3.2 API设计
我们继续沿用队列接口Queue
类声明:
public class IndexPriorityQueue<E extends Comparable<E>> implements Queue<E>, Serializable
内部迭代器类:
private class Itr implements Iterator<E>
成员变量:
访问控制和类型 | 名称 | 描述 |
---|---|---|
private E[] | table | 存放元素数组 |
private int[] | pq | 存放table索引 |
private int[] | qp | pq的逆数组 |
private int | size | 元素个数 |
private int | maxSize | 最大元素个数 |
private Comparator<E> | comparator | 比较器,默认从小到大比较 |
private boolean | max | 是否是最大索引队列,默认false即最小索引队列 |
方法:主要方法,Queue中声明的方法不在列举
访问控制和返回值类型 | 名称 | 描述 |
---|---|---|
public | IndexPriorityQueue(int) | 构造器 |
public | IndexPriorityQueue(int, Comparator<E>, boolean ) | 构造器 |
public E | changeVal(int i, E e) | 更换索引i处元素为e |
public boolean | contains(int i) | 判断索引是否已经存在 |
public void | decreaseVal(int i, E e) | 索引i处优先级降级为e |
public void | increaseVal(int i, E e) | 索引i处元素优先级升级为e |
public void | insert(int i, E e) | 插入元素 |
public void | sink(int i) | 下沉 |
public void | swim(int i) | 上浮 |
3.2 代码实现及简单测试
完整实现代码如下:
import java.io.Serializable;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* @author Administrator
* @date 2022-12-09 20:24
*/
public class IndexPriorityQueue<E extends Comparable<E>> implements Queue<E>, Serializable {
/**
* 存放元素数组
*/
private final E[] table;
/**
* 堆排序数组
*/
private final int[] pq;
/**
* 数组pq的逆数组
*/
private final int[] qp;
/**
* 元素个数
*/
private int size;
/**
* 最大元素个数
*/
private final int maxSize;
/**
* 比较器,默认从小到大排序
*/
private Comparator<E> comparator;
/**
* 是否是最大索引队列,默认不是即最小优先队列
*/
private boolean max = false;
/**
* 传从小到大排序,max指定false;从大到小排序,指定max为true
*/
public IndexPriorityQueue(int maxSize, Comparator<E> comparator, boolean max) {
this(maxSize);
if (comparator != null) {
this.comparator = comparator;
}
this.max = max;
}
public IndexPriorityQueue(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException();
}
this.maxSize = maxSize;
size = 0;
table = (E[]) new Comparable[maxSize];
pq = new int[maxSize + 1];
qp = new int[maxSize];
for (int i = 0; i < maxSize; i++) {
qp[i] = -1;
}
this.comparator = Comparable::compareTo;
}
/**
* 插入元素
*
* @param e 元素
*/
@Override
public void offer(E e) {
throw new UnsupportedOperationException();
}
/**
* 插入元素
*
* @param i 索引i
* @param e 元素e
*/
public void insert(int i, E e) {
validateIndex(i);
if (contains(i)) {
throw new IllegalArgumentException("索引已经存在");
}
if (size >= maxSize) {
throw new IllegalArgumentException("队列已满");
}
size++;
table[i] = e;
pq[size] = i;
qp[i] = size;
swim(size);
}
/**
* 获取队首元素
*
* @return 队首元素
*/
@Override
public E peek() {
validateEmpty();
return table[pq[1]];
}
private void validateEmpty() {
if (size == 0) {
throw new NoSuchElementException();
}
}
/**
* 获取并删除堆有序队首元素
*
* @return 队首元素
*/
@Override
public E poll() {
validateEmpty();
int m = pq[1];
E oldVal = table[m];
exch(1, size--);
sink(1);
qp[m] = -1;
table[m] = null;
pq[size + 1] = -1;
return oldVal;
}
/**
* 判断队列是否为空
*
* @return {@code true}队列为空;反之{@code false}
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* 返回元素个数
*
* @return 元素个数
*/
@Override
public int size() {
return size;
}
/**
* 更换索引i处元素为e
*
* @param i 索引
* @param e 更换后的值
*/
public E changeVal(int i, E e) {
validateIndex(i);
if (!contains(i)) {
throw new NoSuchElementException("队列不存在该索引:" + i);
}
E o = table[i];
table[i] = e;
int j = qp[i];
swim(j);
sink(j);
return o;
}
/**
* 索引i处优先级降级为e
*
* @param i 索引
* @param e 目标值
*/
public void decreaseVal(int i, E e) {
validateIndex(i);
if (!contains(i)) {
throw new NoSuchElementException("队列不存在该索引:" + i);
}
if (table[i].compareTo(e) == 0) {
throw new IllegalArgumentException(e + " 优先级不能等于 " + table[i]);
}
if (table[i].compareTo(e) < 0) {
throw new IllegalArgumentException(e + " 优先级不能高于原优先级 " + table[i]);
}
table[i] = e;
if (max) {
sink(qp[i]);
} else {
swim(qp[i]);
}
}
/**
* 索引i处元素优先级升级为e
*
* @param i 索引i
* @param e 目标值
*/
public void increaseVal(int i, E e) {
validateIndex(i);
if (!contains(i)) {
throw new NoSuchElementException("队列不存在该索引:" + i);
}
if (table[i].compareTo(e) == 0) {
throw new IllegalArgumentException(e + " 优先级不能等于 " + table[i]);
}
if (table[i].compareTo(e) > 0) {
throw new IllegalArgumentException(e + " 优先级不能低于原优先级 " + table[i]);
}
table[i] = e;
table[i] = e;
if (max) {
swim(qp[i]);
} else {
sink(qp[i]);
}
}
/**
* 判断索引是否已经存在
*
* @param i 元素索引
* @return {@code true}逆数组包含该索引;反之{@code false}
*/
public boolean contains(int i) {
validateIndex(i);
return qp[i] != -1;
}
/**
* 删除索引i处的元素
*
* @param i 索引i
* @return 删除的元素
*/
public E delete(int i) {
validateIndex(i);
if (!contains(i)) {
throw new NoSuchElementException("队列不存在该索引: " + i);
}
int k = qp[i];
exch(k, size--);
swim(k);
sink(k);
E o = table[k];
table[k] = null;
qp[k] = -1;
pq[size + 1] = -1;
return o;
}
/**
* 交换元素
*
* @param i 索引i
* @param j 索引j
*/
private void exch(int i, int j) {
int p = pq[i];
pq[i] = pq[j];
pq[j] = p;
qp[pq[i]] = i;
qp[pq[j]] = j;
}
/**
* 比较索引i和索引j处的值
*
* @param i
* @param j
* @return
*/
private boolean compare(int i, int j) {
return comparator.compare(table[pq[i]], table[pq[j]]) < 0;
}
/**
* 返回索引i处的值
*
* @param i 索引
* @return i处的值
*/
public E valueOf(int i) {
validateIndex(i);
if (!contains(i)) {
throw new NoSuchElementException("索引不在优先队列中");
}
return table[i];
}
/**
* 返回最低(高)优先级元素对应的索引
*
* @return 最低(高)优先级元素对应的索引
*/
public int firstIndex() {
validateEmpty();
return pq[1];
}
/**
* 下沉
*
* @param i 下沉起始索引
*/
public void sink(int i) {
while (2 * i <= size) {
int j = 2 * i;
if (j < size && compare(j + 1, j)) {
j++;
}
if (!compare(j, i)) {
break;
}
exch(i, j);
i = j;
}
}
/**
* 上浮
*
* @param i 起始索引
*/
public void swim(int i) {
while (i > 1 && compare(i, i / 2)) {
exch(i, i / 2);
i /= 2;
}
}
/**
* 校验索引i
*
* @param i 索引i
*/
private void validateIndex(int i) {
if (i < 0) {
throw new IllegalArgumentException("index is negative: " + i);
}
if (i >= maxSize) {
throw new IllegalArgumentException("index >= capacity: " + i);
}
}
@Override
public String toString() {
Iterator<E> iterator = iterator();
if (!iterator.hasNext())
return "[]";
StringBuilder b = new StringBuilder("[");
while (true) {
E next = iterator.next();
b.append(next);
if (!iterator.hasNext()) {
return b.append("]").toString();
}
b.append(",");
}
}
@Override
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
// create a new pq
private IndexPriorityQueue<E> copy;
public Itr() {
copy = new IndexPriorityQueue<E>(maxSize, comparator, max);
for (int i = 1; i <= size; i++) {
copy.insert(pq[i], table[pq[i]]);
}
}
@Override
public boolean hasNext() {
return !copy.isEmpty();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public E next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return copy.poll();
}
}
}
测试代码如下:
public class TestIndexPq {
public static void main(String[] args) {
IndexPriorityQueue<Integer> ipq = new IndexPriorityQueue<Integer>(20, (o1, o2) -> o2.compareTo(o1), true);
// Random random = new Random();
// int[] a = new int[10];
// for (int i = 0; i < 10; i++) {
// a[i] = random.nextInt(1000);
// }
int[] a = {317, 430, 734, 880, 105, 536, 536, 813, 48, 806};
for (int i = 0; i < a.length; i++) {
ipq.insert(i, a[i]);
}
// System.out.println(Arrays.toString(a));
// ipq.increaseVal(9, 881);
System.out.println(ipq);
ipq.changeVal(1, 1);
System.out.println(ipq);
// for (Integer integer : ipq) {
// System.out.println(integer);
// }
}
}
5 主要方法讲解
5.1 exch()
该方法用于在堆排序时交换元素,因为我们是通过pq数组来进行的堆排序,那么在交换pq元素的的同时,需要调整qp数组相应值。
方法执行流程如下:
- 交换pq索引i,j的值
- 那么qp对应的索引就是pq[i],pq[j],值替换为i,j
5.2 insert()
这个索引指的就是存储元素数组table的索引,也是qp的索引
执行流程如下:
- 校验索引范围
- 校验索引是否已经存在
- 判断队列是否已满
- 元素大小size+1
- 数组table[i]设置为e
- pq添加至数组末尾,qp设置相应的值
- pq数组堆排序
5.2 poll()
方法目标就是获取堆有序的堆顶元素,并删除
执行流程如下:
- 校验队列是否为空
- 获取堆顶元素在table中的索引
- 记录元素值
- 交换pq中堆顶和堆底的元素,并进行下沉排序
- 删除堆顶元素,破坏了堆有序,把最后一个放置在堆顶,进行下沉排序
- 做清理工作,同时返回记录的原数值
6 分析
命题Q:在一个大小为N的所以优先队列中,插入元素、改变优先级、删除和删除最小元素操作所需的比较次数和 log N \log N logN成正比。
证明:已知堆中所有路径最长为 ∼ lg N \sim \lg N ∼lgN,从代码中很容易证明这个结论。
主要操作在最坏情况下的成本:
操作 | 比较次数的增长数量级 |
---|---|
insert | log N \log N logN |
changeVal | log N \log N logN |
contains | 1 |
delete | log N \log N logN |
firstIndex | 1 |
poll | log N \log N logN |
注:
- 我们这里没有提供对队列扩容的方法,要实现扩容或者缩容的话记得三个数组同时进行
7 后记
如果小伙伴什么问题或者指教,欢迎交流。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm
[1][美]Robert Sedgewich,[美]Kevin Wayne著;谢路云译.算法:第4版[M].北京:人民邮电出版社,2012.10