索引堆的实现与优化

在之前文章中记述了堆的实现(插入方式建堆、heapify方式建堆以及堆排序) 《 堆的实现及其应用 》 。今天来看看索引堆是个什么东西,对于我们所关心的这个数组而言,数组中的元素位置发生了改变。正是因为这些元素的位置发生了改变,我们才能将其构建为最大堆。 如果元素十分复杂的话,比如像每个位置上存的是一篇上万字的文章。那么交换它们之间的位置将产生大量的时间消耗。并且由于数组元素的位置在构建成堆之后发生了改变,那么我们就很难索引到它,很难去改变它。可以在每一个元素上再加上一个属性来表示原来的位置可以解决,但是这样的话,必须将这个数组遍历一下才能解决。针对以上问题,我们就需要引入索引堆(Index Heap)的概念。

索引堆基本实现

对于索引堆来说,我们将数据和索引这两部分分开存储。真正表征堆的这个数组是由索引这个数组构建成的。

mark

而在构建堆(以最大索引堆为例)的时候,比较的是data中的值(即原来数组中对应索引所存的值),构建成堆的却是index域。而构建完之后,data域并没有发生改变,位置改变的是index域。

mark

那么现在这个最大堆该怎么解读呢?例如,堆顶元素为Index=10代表的就是索引为10的data域的值,即62。这时我们来看,构建堆的过程就是简单地索引之间的交换,索引就是简单的int型。效率很高。

现在如果我们想对这个数组进行一些改变,比如我们想将索引为7的元素值改为100,那我们需要做的就是将索引7所对应data域的28改为100。时间复杂度为O(1)。当然改完之后,我们还需要进行一些操作来维持最大堆的性质。不过调整的过程改变的依旧是index域的内容。

  1package imooc.heap;
  2
  3public class MaxIndexHeap {
  4    protected int[] data;
  5    protected int[] indexes;
  6
  7    //堆里有多少元素
  8    protected int count;
  9
 10    //堆的容量
 11    protected int capacity;
 12
 13    //因为0号位置不使用,所以capacity + 1
 14    public MaxIndexHeap(int capacity) {
 15        data = new int[capacity + 1];
 16        indexes = new int[capacity + 1];
 17        count = 0;
 18        this.capacity = capacity;
 19    }
 20
 21    public MaxIndexHeap(int[] arr){
 22        data = new int[arr.length + 1];
 23        capacity = arr.length + 1;
 24        for (int i = 0; i < arr.length; i++) {
 25            data[i + 1] = arr[i];
 26        }
 27        count = arr.length;
 28        //从第一个不是叶子节点的位置开始
 29        for (int i = count / 2; i >= 1; i--) {
 30            shiftDown(i);
 31        }
 32    }
 33
 34    //获取现存元素个数
 35    public int size(){
 36        return count;
 37    }
 38
 39    //判断是否为空
 40    public boolean isEmpty(){
 41        return count == 0;
 42    }
 43
 44    //插入数据(传入的i对于用户而言,是从0开始索引的)
 45    public void insert(int i, int item){
 46        assert i + 1 >= 1;
 47        i++; //i += 1
 48        //判断容量知否超出
 49        if(count + 1 >= capacity){
 50            //开始扩容
 51            resize();
 52        }
 53        //先存储到末尾
 54        data[i] = item;
 55        indexes[count + 1] = i;
 56        count++;
 57        //开始向上调堆
 58        shiftUp(count);
 59    }
 60
 61    //取出数据的索引
 62    public int extractIndexMax(){
 63        if(count == 0) throw new RuntimeException("Heap is null");
 64        int ret = indexes[1] - 1;
 65        swapIndexes(1, count);
 66        count--;
 67        //开始向下调堆
 68        shiftDown(1);
 69        return ret;
 70    }
 71
 72    //根据索引获得元素
 73    public int getItemByIndex(int index){
 74        return data[index];
 75    }
 76
 77    //根据索引修改某个元素
 78    public void changeItem(int index, int newValue){
 79        index++;
 80        data[index] = newValue;
 81        //找到indexes[j] = index; j表示data[index]在堆中的位置
 82        //找到shiftUp(j),再shiftDown(j)
 83        for (int j = 1; j <= count; j++) {
 84            if(indexes[j] == index){
 85                shiftUp(j);
 86                shiftDown(j);
 87                return;
 88            }
 89        }
 90    }
 91
 92    //向下调堆
 93    private void shiftDown(int k) {
 94        while (2 * k <= count){
 95            int j = 2 * k;
 96            if(j + 1 <= count && data[indexes[j+1]] > data[indexes[j]]){
 97                j++;
 98            }
 99            if(data[indexes[k]] >= data[indexes[j]]){
100                break;
101            }
102            swapIndexes(k, j);
103            k = j;
104        }
105    }
106
107    //向上调堆
108    private void shiftUp(int k) {
109        while(k > 1 && data[indexes[k / 2]] < data[indexes[k]]){
110            swapIndexes(k/2, k);
111            k /= 2;
112        }
113    }
114
115    //交换对应两个位置的值(这是其实是交换索引的位置)
116    private void swapIndexes(int i, int j){
117        int tmp = indexes[i];
118        indexes[i] = indexes[j];
119        indexes[j] = tmp;
120    }
121
122    //扩充容量
123    private void resize() {
124        int[] newData = new int[capacity * 2];
125        System.arraycopy(data, 0, newData, 0, count);
126        data = newData;
127        capacity *= 2;
128
129        int[] newIndexes = new int[capacity * 2];
130        System.arraycopy(indexes, 0, newIndexes, 0, count);
131        indexes = newIndexes;
132    }
133}

索引堆的优化:反向查找

如何优化呢? 反向查找:再建立一个数组,这个数组的下标和原始数据数组的下标的意思是一样的,就是索引的意思。而数组中存储的元素则是索引在索引堆数组中的位置。

mark

对反向查找表的维护就是,将索引堆中的值取出来(值就是索引值),这个值就是方向查找表的下标,那这个下标应该对应的元素就是索引堆中的位置。

mark

  1public class MaxIndexHeapOptimize {
  2    protected int[] data;
  3    protected int[] indexes;
  4    protected int[] reverse;
  5
  6    //堆里有多少元素
  7    protected int count;
  8
  9    //堆的容量
 10    protected int capacity;
 11
 12    //因为0号位置不使用,所以capacity + 1
 13    public MaxIndexHeapOptimize(int capacity) {
 14        data = new int[capacity + 1];
 15        indexes = new int[capacity + 1];
 16        reverse = new int[capacity + 1];
 17        count = 0;
 18        for (int i = 0; i <= capacity ; i++) {
 19            reverse[i] = 0;
 20        }
 21        this.capacity = capacity;
 22    }
 23
 24    public MaxIndexHeapOptimize(int[] arr){
 25        data = new int[arr.length + 1];
 26        capacity = arr.length + 1;
 27        for (int i = 0; i < arr.length; i++) {
 28            data[i + 1] = arr[i];
 29        }
 30        count = arr.length;
 31        //从第一个不是叶子节点的位置开始
 32        for (int i = count / 2; i >= 1; i--) {
 33            shiftDown(i);
 34        }
 35    }
 36
 37    //获取现存元素个数
 38    public int size(){
 39        return count;
 40    }
 41
 42
 43    //判断是否为空
 44    public boolean isEmpty(){
 45        return count == 0;
 46    }
 47
 48    //插入数据(传入的i对于用户而言,是从0开始索引的)
 49    public void insert(int i, int item){
 50        assert i + 1 >= 1;
 51        i++; //i += 1
 52        //判断容量知否超出
 53        if(count + 1 >= capacity){
 54            //开始扩容
 55            resize();
 56        }
 57        //先存储到末尾
 58        data[i] = item;
 59        indexes[count + 1] = i;
 60        reverse[i] = count + 1;
 61        count++;
 62        //开始向上调堆
 63        shiftUp(count);
 64    }
 65
 66    //取出数据的索引
 67    public int extractIndexMax(){
 68        if(count == 0) throw new RuntimeException("Heap is null");
 69        int ret = indexes[1] - 1;
 70        swapIndexes(1, count);
 71        reverse[indexes[1]] = 1;
 72        reverse[indexes[count]] = 0;
 73        count--;
 74        //开始向下调堆
 75        shiftDown(1);
 76        return ret;
 77    }
 78
 79    //取出数据的索引
 80    public int extractMax(){
 81        if(count == 0) throw new RuntimeException("Heap is null");
 82        int ret = data[indexes[1]];
 83        swapIndexes(1, count);
 84        reverse[indexes[1]] = 1;
 85        reverse[indexes[count]] = 0;
 86        count--;
 87        //开始向下调堆
 88        shiftDown(1);
 89        return ret;
 90    }
 91
 92    //根据索引获得元素
 93    public int getItemByIndex(int index){
 94        if(contain(index)){
 95            throw new RuntimeException("This index is not in heap!");
 96        }
 97        return data[index];
 98    }
 99
100    //根据索引修改某个元素
101    public void changeItemOld(int index, int newValue){
102        index++;
103        data[index] = newValue;
104        //找到indexes[j] = index; j表示data[index]在堆中的位置
105        //找到shiftUp(j),再shiftDown(j)
106        for (int j = 1; j <= count; j++) {
107            if(indexes[j] == index){
108                shiftUp(j);
109                shiftDown(j);
110                return;
111            }
112        }
113    }
114
115    //根据索引修改某个元素
116    public void changeItem(int index, int newValue){
117        if(contain(index)){
118            throw new RuntimeException("This index is not in heap!");
119        }
120        index++;
121        data[index] = newValue;
122        //找到indexes[j] = index; j表示data[index]在堆中的位置
123        int j = reverse[index]; //O(1)的时间复杂度
124        shiftUp(j);
125        shiftDown(j);
126    }
127
128    private boolean contain(int index) {
129        if(!(index + 1 >= 0 && index + 1 <= capacity)){
130            throw new RuntimeException("This index is illegal");
131        }
132        return reverse[index + 1] == 0;
133    }
134
135    //向下调堆
136    private void shiftDown(int k) {
137        while (2 * k <= count){
138            int j = 2 * k;
139            if(j + 1 <= count && data[indexes[j+1]] > data[indexes[j]]){
140                j++;
141            }
142            if(data[indexes[k]] >= data[indexes[j]]){
143                break;
144            }
145            swapIndexes(k, j);
146            reverse[indexes[k]] = k;
147            reverse[indexes[j]] = j;
148            k = j;
149        }
150    }
151
152    //向上调堆
153    private void shiftUp(int k) {
154        while(k > 1 && data[indexes[k / 2]] < data[indexes[k]]){
155            swapIndexes(k/2, k);
156            reverse[indexes[k/2]] = k/2;
157            reverse[indexes[k]] = k;
158            k /= 2;
159        }
160    }
161
162    //交换对应两个位置的值(这是其实是交换索引的位置)
163    private void swapIndexes(int i, int j){
164        int tmp = indexes[i];
165        indexes[i] = indexes[j];
166        indexes[j] = tmp;
167    }
168
169    //扩充容量
170    private void resize() {
171        int[] newData = new int[capacity * 2];
172        System.arraycopy(data, 0, newData, 0, count);
173        data = newData;
174        capacity *= 2;
175
176        int[] newIndexes = new int[capacity * 2];
177        System.arraycopy(indexes, 0, newIndexes, 0, count);
178        indexes = newIndexes;
179    }
180}

其他和堆相关的问题

1、使用堆来实现优先队列

动态选择优先级最高的任务执行

2、实现多路归并排序

将整个数组分成n个子数组,子数组排完序之后,将每个子数组中最小的元素取出,放到一个最小堆里面,每次从最小堆里取出最小值放到归并结束的数组中,被取走的元素属于哪个子数组,就从哪个子数组中再取出一个补充到最小堆里面,如此循环,直到所有子数组归并到一个数组中。