现在有这样一个数组:
[0.2, 0.1, 0.3, 0.4]
对它进行排序,得:
[0.1, 0.2, 0.3, 0.4]
OK,数组已经排好序了。但是如果此时我想得到排序前的第一个数,该怎么办呢?
Anyway,这种场景是有的。比如索引堆这种数据结构就会有这个需求。
索引数组Now,添加一个索引数组来实现上面的需求:
indexes: [0, 1, 2, 3]<br>
arr: [0.2, 0.1, 0.3, 0.4]
在没有索引数组时,我们需要交换arr
数组中的元素来实现排序。比如这个Case就是交换0.2与0.1。
而有了索引数组后,我们交换的只是arr
中元素的索引。比如这个Case就是把0.2的索引0与 0.1的索引1进行交换。
public void swap(int i, int j) {
int temp = indexes[i];
indexes[i] = indexes[j];
indexes[j] = temp;
}
交换后的两个数组:
indexes: [1, 0, 2, 3]<br>
arr: [0.2, 0.1, 0.3, 0.4]
So,回答刚开始的问题:如果此时我想得到排序前的第一个数,该怎么办呢?
答:arr[0]
。
那如果我想得到排序后的第一个数,该怎么办呢?
1 查询答:先从indexes
中取得排序后的索引,再去arr
中取。
public double get(int i) {
return arr[indexes[i]];
}
2 删除
这里我们用极大值表示没有元素,arr:
indexes: [1, 0, 2, 3]<br>
arr: [0.2, 0.1, 0.3, 0.4]<br>
size: 4
现在要删除排序后第i大的元素(i从0开始计):
public void remove(int i) {
arr[indexes[i]] = Integer.MAX_VALUE;
// 重新排序
size--;
}
执行remove(0)
后:
indexes: [0, 2, 3, 1]<br>
arr: [0.2, MAX, 0.3, 0.4]<br>
size: 3
可以看到indexes做了相应的调整,那么indexes
为什么也要调整呢?因为索引数组的应用场景通常都是维护arr
的初始位置和有序位置。如果不更新indexex
数组,那么后续就无法正常地操作arr
的有序位置。
indexes数组不用删除被删除的索引,直接通过重新排序的交换操作,替换到最后面即可。因为size限制了后面不会被访问到。
3 插入目前只有4个元素且已排好序:
indexes: [1, 0, 2, 3, 4, 5]<br>
arr: [0.2, 0.1, 0.3, 0.4, MAX, MAX]<br>
size: 4
插入0.6:
indexes: [1, 0, 2, 3, 4, 5]<br>
arr: [0.2, 0.1, 0.3, 0.4, 0.6, MAX]<br>
size: 5
插入0.5:
4 更新indexes: [1, 0, 2, 3, 5, 4]<br>
arr: [0.2, 0.1, 0.3, 0.4, 0.6, 0.5]<br>
size: 6
public void update(int i, double data) {
arr[indexes[i]] = data;
}
反向索引
接着上面的Case,我们现在能够获得类似于这样的数据:arr排序后,第2大的数
arr[indexes[1]]
而现在有这样一个需求:我想知道原来arr数组中第i个位置,排好序后在哪个位置。应该怎样做?
常规的方法是遍历indexes
数组,像这样:
public int findIndex(int i) {
for(int j = 0; j < size; j--) {
if([indexes[j] == i)
return j;
}
return -1; // 未找到
}
比如findIndex(0)
的返回值为1。
那么有没有什么方法可以提高性能呢?
有,那就是再一用一个数组reverses
,作为反向索引。反向索引存放的数据通俗来讲就是这样:
reverses[i] == findIndex(i)
而通过上面的代码我们可以看到:
- reverses[i] == j
- indexes[j] == i
进而推导出:
- reverses[indexes[i]] = i;
- indexes[reverses[i]] = i;
初始化:
indexes: [0, 1, 2, 3]<br>
arr: [0.2, 0.4, 0.1, 0.3]<br>
reverses: [0, 1, 2, 3]
排序后:
反向索引的维护indexes: [2, 0, 3, 1]<br>
arr: [0.2, 0.4, 0.1, 0.3]<br>
reverses: [1, 3, 0, 2]
虽然使用反向索引提高了某些时候的查询效率,但会使得程序变得更加复杂。因为在插入和删除时都要维护这个数组。
删除
接上面Case,删除前的arr:
indexes: [2, 0, 3, 1]<br>
arr: [0.2, 0.4, 0.1, 0.3]<br>
reverses: [1, 3, 0, 2]
size: 4
执行remove(0)
后:
indexes: [0, 3, 1, 2]<br>
arr: [0.2, 0.4, MAX, 0.3]<br>
reverses: [0, 2, 3, 1]<br>
size: 3
同indexes一样,reverses数组也不用删除索引,直接交换到合适位置即可。当然reverses也可以删除索引,这个看具体应用。
插入插入操作也类似,这里就不赘述。
核心思想核心思想是:不管任何操作,都要维护indexes
数组和reverse
数组的性质。
一个最小索引堆的代码示例:
public class MinIndexHeap<T extends Comparable<T>> {
private T[] datas; // 存放数据的数组 datas[1..n]
private int[] indexes; // 索引数组 indexes[1..n]
private int[] reverse; // 反向索引 reverse[1..n]
private int count = 0; // 计数
public MinIndexHeap(int capacity) {
this.datas = (T[]) new Comparable[capacity + 1];
this.indexes = new int[capacity + 1];
this.reverse = new int[capacity + 1];
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
// 取最小元素
public T getMin() {
return datas[indexes[1]];
}
// 弹出最小元素
public T popMin() {
if (isEmpty())
throw new RuntimeException("堆为空!");
T res = datas[indexes[1]];
exchange(1, count--);
sink(1);
return res;
}
// 弹出最小元素,并返回其索引
public int popMinIndex() {
if (isEmpty())
throw new RuntimeException("堆为空!");
int res = indexes[1] - 1;
exchange(1, count--);
sink(1);
return res;
}
// 获取原数组i位置元素
public T get(int i) {
return datas[i + 1];
}
// 插入
public void insert(int i, T data) {
i += 1;
if (!contain(i)) {
datas[i] = data;
indexes[count + 1] = i;
reverse[i] = count + 1;
count++;
swim(count);
} else {
datas[i] = data;
int j = reverse[i];
swim(j);
sink(j);
}
}
// 判断是否存在
private boolean contain(int i) {
if (i < 1 || i > datas.length - 1)
throw new RuntimeException("下标非法");
if (i > count) return false;
return datas[i] != null;
}
private boolean less(int i, int j) {
if (datas[indexes[i]] == null) return false;
if (datas[indexes[j]] == null) return true;
return datas[indexes[i]].compareTo(datas[indexes[j]]) < 0;
}
private void exchange(int i, int j) {
int temp = indexes[i];
indexes[i] = indexes[j];
indexes[j] = temp;
reverse[i] = i;
reverse[j] = j;
}
private void swim(int k) {
while (k > 1 && less(k, k / 2)) {
exchange(k, k / 2);
k /= 2;
}
}
private void sink(int k) {
while (2 * k <= count) {
int j = 2 * k;
// 判断是否超出范围,再判断j 和 j + 1 那个更需要上浮
if (j < count && less(j + 1, j))
j++; // 跟需要上浮的那个换
if (less(j, k))
exchange(k, j);
else break;
k = j;
}
}
}