二叉堆的实现见:Java中的优先队列——二叉堆
索引二叉堆又称为最小索引优先队列。它的特点是堆元素位置不变。
保存堆元素的数组不变,即不交换数组中任意元素的位置。利用一个额外的指向堆元素下标(或索引)的索引数组(pq
)来代替它进行交换,同时为了操作方便,再维护一个pq
的翻转数组(pq
中的值作为新数组的索引,索引作为新数组的值)。
文字看起来有点懵就对了。我们直接看它的结构。
假设我们按{4,2,3,1,6,5,7}的顺序插入,并且一旦插入结束之后,不会交换元素的位置。我们又想进行取堆顶元素等操作,怎么办?
可以新增一个特殊的数组(pq
)来维护keys
数组的索引,根据keys
数组中元素的大小移动pq
数组的位置。使得可以通过pq[1]
得到最小元素。
pq
维护的是keys
数组的索引。因此pq[1]
的值一定是keys
数组中最小值的索引,上图中最小值为1,其索引为3。因此pq[1] = 3
。
那么取堆顶元素可以:keys[pq[1]]
。
当构建完成索引二叉堆后,pq
的结构如下:
pq
相当是二叉堆对应的数组,按惯例,该索引从1开始。不过,这个数组中的元素不是堆元素的值,而是堆元素在keys
数组中的下标。
pq
和keys
的关系如下:
其实很简单,pq
的值指向keys
中对应的下标即可。因此,我们就可以根据keys
中相应值的大小调整pq
数组来达到使keys
数组满足堆序性的目的。
索引二叉堆中还有一个特殊的翻转数组。翻转的就是pq
,我们命名这个翻转数组为reversed
。
pq
中的值作为翻转数组的索引,索引作为翻转数组的值。根据上面的例子,其翻转数组如下:
为什么要这个翻转数组呢,其实它只是一个辅助数组,比如我们要得到keys
数组索引5
(keys
数组的索引称为关联索引)在pq
中对应的索引。那么就可以通过reversed[5]
得到。该值为6
,而pq[6] = 5
。
总结一下,它们之间满足这样一个等式:pq[reversed[i]] = reversed[pq[i]] = i
其中,i
为keys
数组的索引。
理解了这个结构,再去理解它的实现代码就不难了。
package com.algorithms.heap;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 索引二叉堆
* 具有堆元素位置不变的性质
*
* 保存堆元素的数组不变,即不交换数组中任意元素的位置。利用一个额外的指向堆元素下标的索引数组(pq)来代替它进行交换,
* 同时为了操作方便,再维护一个pq的翻转数组(pq中的值作为新数组的索引,索引作为新数组的值)
*
* @author yjw
* @date 2019/6/3/003
*/
@SuppressWarnings("unchecked")
public class IndexMinPQ<Key extends Comparable<? super Key>> implements Iterable<Integer> {
/**
* 堆中最大元素数量
*/
private final int capacity;
/**
* 堆中元素数量
*/
private int size = 0;
/**
* 指向堆元素引用的数组
* 数组中的元素指向堆元素(keys)的索引
* 数组里面只是索引,具体参与比较的还是对应的堆元素
*
* 该数组满足堆性质,很容易知道最小元素为keys[pq[1]]
*
* 索引范围: (0,size]
*/
private int[] pq;
/**
* 保存的是关联索引i -> pq中对应的索引,其下标就是关联索引
*
* 以pq中值作为reversed的索引,pq值对应的索引作为reversed的值。
* 相当于将pq数组值和下标翻转过来得到的数组。
*
* 满足: reversed[pq[i]] = pq[reversed[i]] = i
*
* 索引范围: [0,size - 1)
*/
private int[] reversed;
/**
* 堆元素数组,该数组不会进行交换操作
*
* 索引范围: [0,size - 1)
*/
private Key[] keys;
public IndexMinPQ(int capacity) {
if (capacity < 0) {
throw new IllegalStateException();
}
this.capacity = capacity;
keys = (Key[]) new Comparable[capacity + 1];
pq = new int[capacity + 1];
reversed = new int[capacity + 1];
Arrays.fill(reversed, -1);//-1表示没有关联任何pq的索引
}
private void rangeCheck(int i) {
if (i < 0 || i >= capacity) {
throw new IndexOutOfBoundsException();
}
}
/**
* @param i 索引
* @return 该索引是否在堆中
*/
public boolean contains(int i) {
rangeCheck(i);
return reversed[i] != -1;
}
public int size() {
return size;
}
/**
* 后面所说的索引i(public 方法参数中的i)都指的是keys数组中的索引,也就是关联索引
*/
/**
* 关联元素e与索引i
*
* 想查询i对应的元素,除了keys[i],还可以通过keys[pq[reversed[i]]]
*
* @param i
* @param key
*/
public void insert(int i, Key key) {
if (contains(i)) {
throw new IllegalStateException("index is already in the heap");
}
size++;
keys[i] = key;
pq[size] = i;//将索引添加到pq中最后的位置,保存新元素e在keys中的索引
reversed[i] = size;//翻转pq
swim(size);//上滤
}
/**
* 删除最小的元素并返回它的关联索引
*
* @return
*/
public int deleteMin() {
if (isEmpty()) {
throw new NoSuchElementException();
}
//pq下标为1的元素即keys中最小元素的关联索引,
int min = pq[1];
swap(1, size--);//用最后一个元素替代最小元素的位置
sink(1);//下滤
reversed[min] = -1; // 标记为已删除
keys[min] = null; // 防止内存泄漏!!
pq[size + 1] = -1; // 标记之前的最后一个元素为已删除
return min;
}
/**
* 删除关联索引i处的元素,注意删除的是keys[i]对应的元素
*
* @param i
*/
public void delete(int i) {
if (!contains(i)) {
throw new NoSuchElementException();
}
/**
* 得到pq数组中的索引index,然后删除keys[i]处的元素
*/
int index = reversed[i];
/**
* 同样用最后一个元素替代该元素
*/
swap(index, size--);
/**
* 这里需要进行上滤和下滤操作
* 有可能堆中最后的元素小于index处的父节点
*/
swim(index);
sink(index);
keys[i] = null;//防止内存泄漏
reversed[i] = -1;//表明关联索引i没有关联任何元素了
}
/**
* 返回关联最小元素的索引
*
* @return
*/
public int minIndex() {
if (isEmpty()) {
throw new NoSuchElementException();
}
return pq[1];
}
public Key minKey() {
return keys[minIndex()];
}
/**
* 返回与索引i关联的元素
*
* @param i
* @return
*/
public Key keyOf(int i) {
if (!contains(i)) {
throw new NoSuchElementException();
}
return keys[i];
}
public boolean isEmpty() {
return size == 0;
}
private boolean less(int i, int j) {
return keys[pq[i]].compareTo(keys[pq[j]]) < 0;
}
/**
* 将i关联的元素值增加到 key
*
* @param i
* @param key 要满足大于i处的值
*/
public void increaseKey(int i, Key key) {
if (!contains(i)) {
throw new NoSuchElementException();
}
if (keys[i].compareTo(key) >= 0) {
throw new IllegalArgumentException("Calling increaseKey() with given argument would " +
"not strictly increase the key");
}
keys[i] = key;
//因为是增大值,所以只需要下滤
sink(reversed[i]);
}
public void decreaseKey(int i, Key key) {
if (!contains(i)) {
throw new NoSuchElementException();
}
if (keys[i].compareTo(key) <= 0) {
throw new IllegalArgumentException("Calling decreaseKey() with given argument " +
"would not strictly decrease the key");
}
keys[i] = key;
//上滤
swim(reversed[i]);
}
public void changeKey(int i, Key key) {
if (!contains(i)) {
throw new NoSuchElementException();
}
keys[i] = key;
//因为不知道是增加了还是减少了,所以需要在两个方向进行交换
swim(reversed[i]);
sink(reversed[i]);
}
/**
* 另一种写法
* @param i
* @param key
*/
/*public void changeKey(int i, Key key) {
if (!contains(i)) {
throw new NoSuchElementException();
}
if (keys[i].compareTo(key) < 0) {
increaseKey(i, key);
} else if (keys[i].compareTo(key) > 0) {
decreaseKey(i, key);
}
}*/
// swim/swap/sink都是对pq和reversed数组进行的,keys数组只能进行置null操作!!!
/**
* 上滤
*
* @param k
*/
private void swim(int k) {
/**
* 当k比它的父节点要小时,上滤
* 直到k不小于它的父节点
* 或k变成了堆顶(k=1)
*/
while (k > 1 && less(k, k / 2)) {
swap(k, k / 2);
k = k / 2;
}
}
/**
* 下滤
*
* @param k
*/
private void sink(int k) {
//如果有孩子,不停的下滤,直到满足堆的性质
//k * 2 <= size 说明至少有左孩子
while (k * 2 <= size) {
int child = k * 2;
//如果有右孩子(child +1),且右孩子更小
if (child < size && less(child + 1, child)) {
child = child + 1;
}
if (less(k, child)) {
//如果小于孩子,就不用下滤了
break;
}
swap(k, child);
k = child;//更新k
}
}
/**
* 交换pq和reversed数组
*
* @param i
* @param j
*/
private void swap(int i, int j) {
int tmp = pq[i];
pq[i] = pq[j];
pq[j] = tmp;
reversed[pq[i]] = i;
reversed[pq[j]] = j;
}
@Override
public Iterator<Integer> iterator() {
return new HeapIterator();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("capacity: ").append(capacity).append(",current size: ").append(size).append("\n");
sb.append("keys:").append(Arrays.toString(keys)).append("\n");
sb.append("pq:").append(Arrays.toString(pq)).append("\n");
sb.append("reversed:").append(Arrays.toString(reversed));
return sb.toString();
}
private class HeapIterator implements Iterator<Integer> {
private IndexMinPQ<Key> copied;
public HeapIterator() {
copied = new IndexMinPQ<>(pq.length - 1);
for (int i = 1; i <= size; i++) {
//pq数组已经满足堆序性,因此没有元素会移动
copied.insert(pq[i], keys[pq[i]]);
}
}
@Override
public boolean hasNext() {
return !copied.isEmpty();
}
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return copied.deleteMin();
}
}
public static void main(String[] args) {
Integer[] ints = {4, 2, 3, 1, 6, 5, 7};//假设以这个顺序插入堆
IndexMinPQ<Integer> pq = new IndexMinPQ<>(ints.length);
for (int i = 0; i < ints.length; i++) {
pq.insert(i, ints[i]);
}
System.out.println(pq);
// print each key using the iterator
/*for (int i : pq) {
System.out.println(i + " " + ints[i]);
}*/
}
}