手记

算法技巧-索引与反向索引

现在有这样一个数组:

[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的索引00.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:

indexes: [1, 0, 2, 3, 5, 4]<br>
arr: [0.2, 0.1, 0.3, 0.4, 0.6, 0.5]<br>
size: 6

4 更新
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;
        }
    }
}
3人推荐
随时随地看视频
慕课网APP