索引堆是对堆进行了优化。关于堆的介绍可以查看数据结构--堆。
优化了什么?在堆中,构建堆、插入、删除过程都需要大量的交换操作。在之前的实现中,进行交换操作是直接交换datas
数组中两个元素。而索引堆交换的是这两个元素的索引,而不是直接交换元素。
主要有两个好处:
- 减小交换操作的消耗,尤其是对于元素交换需要很多资源的对象来说,比如大字符串。
- 可以根据原位置找到元素,即便这个元素已经换了位置。
索引堆使用了一个新的int
类型的数组,用于存放索引信息。部分代码如下:
// 属性
T[] datas; // 存放数据的数组 datas[1..n]
int[] indexes; // 索引数组
这里这个indexes
数组,存放的是什么信息呢?它是如何工作的呢?假如我们有这样一个最小堆:
那么用数组表示就是:
datas: [-, 1, 15, 20, 34, 7]
现在要维护最小堆的有序性,就需要交换15
和7
这两个元素。交换之后的元素数组是:
datas: [-, 1, 7, 20, 34, 15]
而此时,我们再想找到原来在datas[2]位置的元素,已经找不到了。因为此时data[2]已经换成了7
,而系统并没有记录15
被换到了什么地方。
这个时候,想要得到i位置的元素,直接datas[i]
就可以了。
使用索引堆
使用索引堆后,初始化两个数组应该是这样的:
- datas: [-, 1, 15, 20, 34, 7]
- indexes: [-, 1, 2, 3, 4, 5]
这个时候,我们就交换indexes
数组里面的索引2
和5
,而不操作datas
数组。交换后两个数组是这个样子:
- datas: [-, 1, 15, 20, 34, 7]
- indexes: [-, 1, 5, 3, 4, 2]
这个时候,想要得到i位置的元素,就需要datas[indexes[i]]
来获取。
索引堆的Java实现代码:
public class MyIndexHeap<T> implements IHeap<T> {
T[] datas; // 存放数据的数组 datas[1..n]
int[] indexes; // 索引数组
private int count = 0; // 计数
private Comparator<T> comparator; // 比较器
// 使用空数组构造,容量为capacity
public MyIndexHeap(int capacity, Comparator<T> comparator) {
this.datas = (T[]) new Object[capacity + 1];
// 为索引开辟空间
this.indexes = new int[capacity + 1];
this.comparator = comparator;
}
// 使用已给的数组构造,容量为数组长度
public MyIndexHeap(T[] datas, Comparator<T> comparator) {
this.count = datas.length;
this.datas = (T[]) new Object[count + 1];
this.indexes = new int[count + 1];
for (int i = 0; i < count + 1; i++) {
indexes[i] = i;
}
this.comparator = comparator;
System.arraycopy(datas, 0, this.datas, 1, count);
// 对前半部分逆序下沉,构造堆
for (int k = count / 2; k >= 1; k--)
sink(k);
// 对最后一层,也就是后半部分,不断交换到堆顶,再下沉
int k = count;
}
@Override
public int size() {
return count;
}
@Override
public boolean isEmpty() {
return count == 0;
}
@Override
public void add(T data) {
if (count == datas.length - 1)
throw new RuntimeException("堆已满");
datas[indexes[++count]] = data;
swim(count);
}
@Override
public T pop() {
return remove(1);
}
@Override
public T remove(int i) {
if (isEmpty())
throw new RuntimeException("堆为空");
if (i < 1 || i > count)
throw new RuntimeException("下标非法");
T res = datas[indexes[i]];
datas[indexes[i]] = datas[indexes[count]];
datas[indexes[count--]] = null; // 防止对象游离
sink(i);
return res;
}
@Override
public T peek() {
if (isEmpty())
throw new RuntimeException("堆为空");
return datas[indexes[1]];
}
/**
* 比较两个数,看谁上浮
* @param i 第一个数
* @param j 第二个数
* @return true 为i上浮, false 为j上浮
*/
private boolean needSwim(int i, int j) {
return comparator.compare(datas[indexes[i]], datas[indexes[j]]) < 0;
}
private void exchange(int i, int j) {
int temp = indexes[i];
indexes[i] = indexes[j];
indexes[j] = temp;
}
/**
* 上浮
* @param k 开始上浮的位置
*/
private void swim(int k) {
while (k > 1 && needSwim(k, k / 2)) {
exchange(k, k / 2);
k /= 2;
}
}
/**
* 下沉
* @param k 开始下沉的位置
*/
private void sink(int k) {
while (2 * k <= count) {
int j = 2 * k;
// 判断是否超出范围,再判断j 和 j + 1 那个更需要上浮
if (j < count && needSwim(j + 1, j))
j++; // 跟需要上浮的那个换
if (needSwim(j, k))
exchange(k, j);
else break;
k = j;
}
}
}