索引堆的实现

   之前讲的基础堆的实现,是将数组元素进行交换最后建成最大堆,这种交换数组元素来建堆的方式有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操作。

  现对最大索引堆的类声明修改如下:


索引最小优先队列(IndexMinPQ)是一种特殊的数据结构,它结合了优先队列和索引数组的特点,允许根据元素的优先级进行高效操作,同时保留对原始数据索引的访问。这种数据结构在图算法(如 Dijkstra 算法)和任务调度中非常有用。 ### 设计思路 IndexMinPQ 的核心在于维护三个数组: - `pq[]`:保存堆中的索引,按照堆的结构排列。 - `qp[]`:保存每个索引在 `pq[]` 中的位置,即逆序索引,用于快速查找某个索引是否在堆中以及其位置。 - `keys[]`:保存每个索引对应的实际键值(优先级)。 堆的操作(如上浮、下沉)将基于 `keys[]` 中的值进行,同时维护 `pq[]` 和 `qp[]` 的一致性。 ### Java 实现示例 以下是一个简化的 `IndexMinPQ` 实现: ```java public class IndexMinPQ<Key extends Comparable<Key>> { private int maxN; // 最大容量 private int n; // 当前元素数量 private int[] pq; // 堆数组,保存索引 private int[] qp; // 逆序数组,qp[i] 表示索引 i 在 pq 中的位置 private Key[] keys; // 键值数组,keys[i] 是索引 i 对应的键 public IndexMinPQ(int maxN) { this.maxN = maxN; n = 0; keys = (Key[]) new Comparable[maxN]; pq = new int[maxN + 1]; qp = new int[maxN]; for (int i = 0; i < maxN; i++) qp[i] = -1; // 初始化为 -1 表示该索引不在队列中 } public boolean isEmpty() { return n == 0; } public boolean contains(int i) { return qp[i] != -1; } public void insert(int i, Key key) { if (contains(i)) throw new IllegalArgumentException("索引已存在"); qp[i] = n + 1; pq[n + 1] = i; keys[i] = key; swim(n + 1); n++; } public int delMin() { int indexOfMin = pq[1]; exch(1, n); sink(1); n--; qp[indexOfMin] = -1; // 标记为不在队列中 keys[pq[n + 1]] = null; // 避免对象游离 pq[n + 1] = -1; return indexOfMin; } public Key minKey() { return keys[pq[1]]; } public void changeKey(int i, Key key) { if (!contains(i)) throw new NoSuchElementException("索引不存在"); Key compareTo = keys[i]; keys[i] = key; if (key.compareTo(compareTo) < 0) { swim(qp[i]); } else { sink(qp[i]); } } private boolean greater(int i, int j) { return keys[pq[i]].compareTo(keys[pq[j]]) > 0; } private void exch(int i, int j) { int swap = pq[i]; pq[i] = pq[j]; pq[j] = swap; qp[pq[i]] = i; qp[pq[j]] = j; } private void swim(int k) { while (k > 1 && greater(k / 2, k)) { exch(k, k / 2); k = k / 2; } } private void sink(int k) { while (2 * k <= n) { int j = 2 * k; if (j < n && greater(j, j + 1)) j++; if (!greater(k, j)) break; exch(k, j); k = j; } } } ``` ### 使用场景 - **图算法**:如 Dijkstra 算法中,需要根据节点的最短路径估计值动态调整优先级。 - **任务调度**:系统需要根据任务的优先级进行调度,同时保留任务的唯一标识。 - **事件驱动模拟**:管理未来事件的时间线,每个事件有其触发时间作为优先级。 ### 特性 - 插入和删除操作的时间复杂度为 O(logN)。 - 支持动态修改某个索引对应的键值。 - 支持判断某个索引是否存在于队列中。 索引优先队列的实现相对复杂,但其在处理需要索引和优先级双重控制的场景下具有显著优势。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值