一、前言
前面已经讲了优先队列(堆)的实现,https://blog.csdn.net/weixin_43696529/article/details/104672731,但是其很明显有一个缺点,那就是无法直接访问已经在队列中的元素,更新或是删除它们,在Dijistra
算法中就非常需要此性质,因此要解决此问题就需用到索引优先队列
二、索引优先队列数据结构讲解
索引优先队列使用一个int[] pq
数组作为索引队列, 保存对象在数组中的位置,使用T[] keys数组保存对象关联的值,使用int[] qp表示对象在索引队列pq中的位置。
如有这样一组数据:
0 -> f
1 -> a
3 -> c
5 -> r
7 -> g
10-> i
我们将0,1,3,5,7,10称为数据对象的索引
对应的pq
、qp
以及keys
数组就是:
0 1 2 3 4 5 6 7 8 9 10
pq: 1 3 0 7 10 5
qp: 2 0 1 5 3 4
keys: f a c r g i
可以看到
keys[i] : i就是数据对象索引,keys[i] 就是与之关联的值 keys[0]=f,keys[1]=a
qp[i] : 对象索引在索引队列中的位置,qp[0]=2 ,对象索引 0 在队列中的位置为2
pq[i] : 即按照对应索引关联的值进行排序,如a是所有元素中最小的,因此其索引1在pq中的位置就是0,排在第一位
画出对应的二叉树会更容易理解:
节点外边的就是pq的下标j,节点中的数字就是对象索引即pq[j]的值,节点中的字符就是 索引关联的对象,保存在keys中
因此对于 1->a 这样一个数据,用 i 表示其索引1
则有以下关系:
令
x=qp[i]
则
pq[x]=i
keys[pq[x]]=a
keys[i] = a
pq[qp[i]] = i
数据结构如下:
public class IndexMinPQ<Key extends Comparable<? super Key>> implements Iterable<Integer> {
/**
* 索引优先队列,保存对象在数组中的位置,按索引值(即keys[i],i为索引)进行小堆排序
*/
private int[] pq;
/**
* pq的逆序,保存对象索引在pq中的位置
*/
private int[] qp;
/**
* 队列最大元素数
*/
private int maxSize;
/**
* 当前队列元素数量
*/
private int currentSize;
/**
* 具体元素
*/
private Key[] keys;
public IndexMinPQ(int maxSize) {
if (maxSize<0){
throw new IllegalArgumentException("参数非法");
}
this.maxSize = maxSize;
pq=new int[maxSize + 1 ];
qp=new int[maxSize + 1];
keys= (Key[]) new Comparable[maxSize+1];
currentSize=0;
for (int i = 0; i < maxSize + 1; i++) {
//qp 初始化为-1 ,表示没有索引关联的对象
qp[i] = -1;
}
}
}
这样就可以得到几个常用的方法:
-
keyOf(int i)
返回索引 i 关联的需public Key keyOf(int i){ checkIndex(i); if (!contains(i))throw new NoSuchElementException("不存在该索引"); return keys[i]; }
-
minIndex()
返回最小的索引/** * 返回最小的索引,即索引队列pq[1]对应的索引对象 * @return */ public int minIndex(){ if (isEmpty()){ throw new NoSuchElementException("队列为空"); } return pq[1]; }
-
contains(int index)
:索引index是否包含在队列中,即index是否关联了对象public boolean contains(int index){ checkIndex(index); //如果存在,则qp[index]一定不为-1,且指向 该对象index在 索引队列pq中的位置 return qp[index] != -1; } private void checkIndex(int index){ if (index<0 || index>= maxSize){ throw new IndexOutOfBoundsException("参数越界"); } }
三、插入操作
同优先队列思想类似。
若插入一对数据为: index -> key
这里首先将对象索引index保存在优先队列pq的最后一个元素(先让currentSize+1),因此 qp[index] = currentSize,pq[currentSize]=index,
keys[index]=key`,接着从优先队列pq的最后一个节点,即刚刚加入的节点开始执行上滤操作(同堆排序中的上滤基本相同),但这里需要注意的是,在上滤的过程中,比较的是 keys[pq[i]],即比较的是索引index关联的值key,发生交换时不光要交换pq的值,还要更新qp的值。
实现如下:
插入:
/**
* 插入一对值
* @param index 索引
* @param key 索引关联的值
*/
public void insert(int index,Key key){
checkIndex(index);
if (contains(index)){
throw new IllegalArgumentException("索引"+index+"已存在");
}
//增加元素数
currentSize++;
//当前对象的索引为队列尾部
qp[index]=currentSize;
//该索引指向的对象在key中的位置为index
pq[currentSize]=index;
keys[index] = key;
//对尾部元素上滤
percolateUp(currentSize);
}
上滤操作:
/**
* 上滤
* @param n
*/
private void percolateUp(int n) {
//将n和n/2(n的父亲)比较,如果父亲大,则交换两个节点
for (; n>1&&compareTo(n,n/2)<0 ; n/=2) {
swap(n,n/2);
}
}
compareTo
比较关联的值:
/**
* 比较索引队列i和j上的对应的key值大小
* @param i
* @param j
* @return
*/
private int compareTo(int i,int j){
return keys[pq[i]].compareTo(keys[pq[j]]);
}
交换pq[i]和pq[j]的元素:
/**
* 交换pq[i]和pq[j]的元素
* 并更新qp,qp[pq[i]]=i;
* @param i
* @param j
*/
private void swap(int i, int j) {
int temp= pq[i];
pq[i] = pq[j];
pq[j] = temp;
qp[pq[i]] = i;
qp[pq[j]] = j;
}
四、删除最小键
删除最小键(即keys[pq[1]])比较简单:
- 获取最小键索引
minIndex
,并将索引队列pq的第一个元素和最后一个元素交换,并让currentSize减一 - 同优先队列一样,从队列第一个元素开始下滤,将第一个元素放在满足堆序的位置
- 此时只需让
qp[minIndex]=-1
,keys[minIndex]=null
即可
实现:
/**
* 删除最小键并返回其关联的索引。
* @return 关联的索引 即pq[1]
*/
public int delMin(){
if (currentSize == 0) {
return -1;
}
int minIndex=pq[1];
swap(1,currentSize--);
percolateDown(1);
//删除当前对象,即pq中不存在该对象了
qp[minIndex] = -1;
pq[currentSize+1]=-1;//不是必须的
keys[minIndex]=null;
return minIndex;
}
下滤:
逻辑同优先队列(堆)
/**
* 下滤
* @param k
*/
private void percolateDown(int k) {
int child;
for (; k*2 <= currentSize ; k =child) {
child= 2 * k;
if (child < currentSize &&compareTo(child+1,child)<0){
child++;
}
if (compareTo(child,k)<0){
swap(child,k);
}else {
break;
}
}
}
五、删除指定索引关联的key
删除指定索引的关联的key同删除最小值基本一样,只是交换时是交换pq[qp[i]]与pq[currentSize]的元素,i为待删除对象索引。
且交换完后不能只进行下滤,因为我们不确定待删除的是pq[1]所关联的对象,因此需要进行上滤和下滤两次操作。
/**
* 删除与索引i关联的key
* @param i
*/
public void delete(int i){
checkIndex(i);
if (!contains(i)){
throw new IllegalArgumentException("索引不存在");
}
int index=qp[i];
swap(index,currentSize--);
//上滤
percolateUp(index);
//再下滤 顺序随意
percolateDown(index);
keys[index]=null;
qp[i] = -1;
}
五、修改索引i关联的key值
因为只是修改了i关联的值,所以只要修改keys[i]=newKey,而 qp[i] 和 pq[qp[i]] 指的值并没有改变。但因为pq[qp[i]]
关联的对象改变了,因此需要对qp[i] 进行上滤和下滤,重新调整索引优先队列。
/**
* 修改索引i关联的key值
* @param i
* @param key
*/
public void changeKey(int i,Key key){
checkIndex(i);
if (!contains(i))throw new NoSuchElementException("不存在该索引");
keys[i]=key;
percolateUp(qp[i]);
percolateDown(qp[i]);
}
六、增大或减小索引i关联的key值
以减小为例,首先要判断新的key是否小于原key值,如果小于,则重新指定keys[i]的值,并进行上滤,因为key比原来的key小,其子节点必然比它小,但是父节点可能会比新的key大,因此只需上滤。
增大与之相反。
/**
* 将与索引 i 关联的键减小为指定值。
* @param i
* @param key
*/
public void decreaseKey(int i,Key key){
checkIndex(i);
if (!contains(i))throw new NoSuchElementException("不存在该索引");
if(keys[i].compareTo(key)<0){
throw new IllegalArgumentException("当前key无法缩小原key值");
}
keys[i]=key;
//当前key变小,因此上滤即可
percolateUp(qp[i]);
}
/**
* 将与索引 i 关联的键增加为指定值。
* @param i
* @param key
*/
public void increaseKey(int i, Key key) {
checkIndex(i);
if (!contains(i))throw new NoSuchElementException("不存在该索引");
if (keys[i].compareTo(key) >= 0)
throw new IllegalArgumentException("当前key无法增加原key值");
keys[i] = key;
//当前key变大,因此下滤即可
percolateDown(qp[i]);
}
参考:
《算法第四版》2.4节