之前讲的基础堆的实现,是将数组元素进行交换最后建成最大堆,这种交换数组元素来建堆的方式有2个局限性:
(1)若数组元素是非常复杂的结构(比如巨大的字符串),那么每次交换这些字符串消耗性能都是巨大的;
(2)若数组成为最大堆以后,我们需要对某个下标下原来的元素进行操作,那么操作将会是很复杂的(比如假设每个下标代表一个进程,每个进程为一个系统任务,下标处对应的元素为该任务的优先级,那么在建成最大堆后,数组元素位置发生改变,这个时候索引和该位置上的元素没有关联,如果我们需要将某个索引处的任务优先级提高,这个时候是很难找到该索引对应的任务优先级的);
为此,引入索引堆的概念来解决这些问题。
索引堆:数组由两部分构成(索引和数据),建堆的时候是对索引进行操作,最后形成的堆中每个节点处的元素就是索引值,而数据的位置没有发生改变。这样,在建堆时交换索引值是很快的,并且在建成堆以后,可以根据索引值找到对应的数组下标处存放的数据。
接下来看索引堆的C++实现:
#include<iostream>
#include<cassert>
using namespace std;
//最大索引堆
template<typename Item>
class IndexMaxHeap
{
private:
Item *data; //数据
int *indexs; //索引
int count;
int capacity;
void shiftUp(int k)
{
int index = indexs[k];
while(k > 1 && data[indexs[k/2]] < data[index])
{
indexs[k] = indexs[k/2];
k /= 2;
}
indexs[k] = index;
}
void shiftDown(int k)
{
int index = indexs[1];
while( 2*k <= count )
{
int j = 2*k;
if(j + 1 <= count && data[indexs[j]] < data[indexs[j + 1]])
{
j += 1;
}
if(data[indexs[k]] >= data[indexs[j]])
{
break;
}
indexs[k] = indexs[j];
k = j;
}
indexs[k] = index;
}
public:
IndexMaxHeap(int capacity)
{
data = new Item[capacity + 1]; //堆从1开始存放
indexs = new int[capacity + 1];
this->capacity = capacity;
count = 0;
}
~IndexMaxHeap()
{
delete[] data;
delete[] indexs;
}
int size() const
{
return count;
}
bool isEmpty() const
{
return count == 0;
}
void insert(int i, Item item) //data[i] = item
{
assert(i >= 0 && i < capacity);
assert(count + 1 <= capacity);
//由于用户给定的数组下标是从0开始,所以对于i我们需要将其加1
i += 1; //对于堆从1开始存放
data[i] = item;
indexs[count + 1] = i;
count++;
shiftUp(count);
}
Item extractMax() //返回最大值
{
assert(count > 0);
Item ret = data[indexs[1]];
indexs[1] = indexs[count];
count--;
shiftDown(1);
return ret;
}
int extractMaxIndex() //返回最大值索引
{
assert(count > 0);
//对于外界来说,希望给出的索引是从0开始的,所以返回值减1
int index = indexs[1] - 1;
indexs[1] = indexs[count];
count--;
shiftDown(1);
return index;
}
Item getItem(int i) //返回给定索引值下的数据
{
assert(i >= 0 && i < capacity);
return data[i + 1];
}
void change(int i, Item newItem) //将索引i下的数据修改为新的数据
{
i += 1;
data[i] = newItem;
for(int j = 1; j <= count; j++)
{
//寻找i在indexs数组中的位置下标
if(indexs[j] == i)
{
//从j这个位置shiftUp和shiftDown操作
shiftUp(j);
shiftDown(j);
return;
}
}
}
void print() const
{
for(int i = 1; i <= count; i++)
{
cout << indexs[i] << " ";
}
cout << endl;
for(int i = 1; i <= count; i++)
{
cout << data[i] << " ";
}
cout << endl;
}
};
注意在change()方法中,寻找索引 i 在indexs数组中的位置 j 时,使用的是for循环遍历indexs数组(时间复杂度为O(n)),找到 j 之后进行shiftUp、shiftDown的操作(时间复杂度为O(logn)),所以这个过程的时间复杂度为O(n),若对 n 个索引堆进行change操作,那么时间复杂度有可能变成O(n^2),这是很不理想的。
于是对方法change进行优化:索引堆由3部分构成(索引 indexs、数据 data、反向索引 rev),其中 rev[ i ] = j 代表索引 i 在 indexs(堆)中的位置下标为 j ;所以我们只需要维护好 rev 数组,那么在修改了索引 i 下的元素后,可以直接通过 rev[ i ] 查找到索引 i 在 indexs中的位置,然后进行相应的shiftUp、shiftDown操作。
现对最大索引堆的类声明修改如下: