跳转至

索引堆

一、什么是索引堆?

索引堆(Index Heap)是二叉堆的一个变种,是二叉堆的一种增强。

相比于二叉堆只能访问堆顶元素,索引堆可以通过索引访问堆中的任意元素。

比如下面的索引堆:

       1(30)      
    /        \   
 4(28)       3(16)  
    \         /    
   5(22)    2(12)    

除了可以直接访问堆顶元素 30,还可以通过索引访问其他元素:

  • 1: 30
  • 2: 12
  • 3: 16
  • 4: 28
  • 5: 22

这相当于给堆元素增加了索引,因此称为索引堆。

二、为什么用索引堆?

二叉堆,每次只能操作堆顶元素,没办法操作其他的元素。

在某些场景下,有时候需要操作一下非堆顶的元素,这种二叉堆就做不到了。

索引堆就是专门为了解决操作非堆顶元素问题而设计的。

三、索引堆如何实现?

一种简单的方式是给元素加上索引属性,但是这需要重新实现一套新代码。

但另一种常见方式是,基于现有的二叉堆来改造实现:

  • 使用额外一个数组记录元素索引,将索引数组建成堆

其结构类似这样子:

                     6(30)     
                  /        \  
               4(28)       3(16)                   -- 索引堆结构
              /    \       /   
            2(18) 5(22)  1(12)   

   ___ ____ ____ ____ ____ ____ ____
  |   | 6  | 4  | 3  | 2  | 5  | 1  |              -- 索引堆数组

   ___ ____ ____ ____ ____ ____ ____
  |   | 12 | 18 | 16 | 28 | 22 | 30 |              -- 元素数组

其中,6(30) 表示索引为 6 的元素,对应的值为 30。

  • 元素数组:按照元素索引保存,元素位置会一直保持不变
  • 索引堆数组:保存的是元素索引,会按照堆的结构来调整位置

有了这两个数组,就能随意获取堆里面的元素了:

  • 获取堆顶元素时,可通过索引堆数组获取
  • 根据索引获取元素时,可通过元素数组获取

这其实相当于在二叉堆的基础上,加了一个中间层,用索引数组来替代元素数组建堆而已。

不过利用这种方式,就可以通过直接改造二叉堆的代码来实现索引堆了。

四、实际案例

问题描述:

多路归并问题,将多个有序的输入流,合并成一个有序的输出流。

解决方案:

这个问题,就可以使用索引堆来解决。

每个堆元素表示一路输入流,堆顶元素就是当前最小的那一路输入流。

// 创建一个最小索引堆(会同时记录输入流的索引和值)
IndexHeap<Integer> indexHeap = new MinIndexHeap<>(n);

// 初始化多路数据
for (int i = 0; i < n; i++) {
    int num = ins[i].readInt();
    indexHeap.insert(i, num);
}

// 对多路数据进行排序
while (indexHeap.size() > 0) {
    // 移除堆顶记录,追加到输出流
    int index = indexHeap.firstIndex();
    int num = indexHeap.removeFirst();
    out.write(num);

    // 某一路数据已经读完了
    if (ins[index] == null) {
       continue;
    }

    // 从被移除的那一路补充数据
    num = ins[index].readInt();
    indexHeap.insert(index, num);
}

合并多路输入流时,需要操作非堆顶的元素。

如果用普通的二叉堆就不好实现了,而索引堆则正好解决这个问题。

参考

《算法(第4版)》

附录

索引堆接口

/**
 * 索引堆接口
 *
 * @author weijiaduo
 * @since 2023/2/25
 */
public interface IndexHeap<T> {

    /**
     * 插入新值
     *
     * @param index 指定索引
     * @param val   新值
     */
    void insert(int index, T val);

    /**
     * 移除第一个节点
     *
     * @return 第一个节点值
     */
    T removeFirst();

    /**
     * 删除指定索引的元素
     *
     * @param index 指定索引
     * @return 被删除元素
     */
    T remove(int index);

    /**
     * @return 第一个元素
     */
    T first();

    /**
     * 根节点元素的索引
     *
     * @return 索引
     */
    int firstIndex();

    /**
     * 指定索引的元素是否存在
     *
     * @param index 指定索引
     * @return true/false
     */
    boolean containsIndex(int index);

    /**
     * @return 元素数量
     */
    int size();

}

索引堆实现

/**
 * 索引堆
 *
 * @author weijiaduo
 * @since 2023/2/25
 */
public class IndexHeapImpl<T extends Comparable<T>> implements IndexHeap<T> {

    /**
     * 元素数组
     */
    private final T[] elements;
    /**
     * 堆数组,堆元素 -> 元素数组索引
     * <p>
     * 另外,heap[0] 不用
     */
    private final int[] heap;
    /**
     * 元素数组索引 -> 堆数组索引
     */
    private final int[] idxMap;
    /**
     * 元素比较器
     */
    private final Comparator<T> cmp;
    /**
     * 元素数量
     */
    private int size;

    public IndexHeapImpl(int capacity) {
        this(capacity, Comparator.reverseOrder());
    }

    public IndexHeapImpl(int capacity, Comparator<T> cmp) {
        this.cmp = cmp;
        //noinspection unchecked
        elements = (T[]) new Comparable[capacity];
        heap = new int[capacity + 1];
        idxMap = new int[capacity];
        Arrays.fill(heap, -1);
        Arrays.fill(idxMap, -1);
    }

    @Override
    public void insert(int idx, T val) {
        if (idx < 0 || idx >= elements.length) {
            throw new IllegalStateException(String.format("size: %d, index: %d", elements.length, idx));
        }

        if (containsIndex(idx)) {
            // 更新元素
            elements[idx] = val;
            int hp = idxMap[idx];
            siftUp(hp);
            siftDown(hp);
        } else {
            // 插入元素
            elements[idx] = val;
            int hp = ++size;
            heap[hp] = idx;
            idxMap[idx] = hp;
            siftUp(hp);
        }
    }

    @Override
    public T removeFirst() {
        if (size <= 0) {
            throw new IllegalStateException("Heap isEmpty!");
        }

        int hp = 1;
        int idx = heap[hp];
        T val = elements[idx];
        swap(hp, size);
        heap[size] = -1;
        idxMap[idx] = -1;
        elements[idx] = null;
        size--;

        siftDown(hp);
        return val;
    }

    @Override
    public T remove(int idx) {
        if (!containsIndex(idx)) {
            throw new IllegalStateException(String.format("size: %d, index: %d", elements.length, idx));
        }

        int hp = idxMap[idx];
        T val = elements[idx];
        swap(hp, size);
        heap[size] = -1;
        idxMap[idx] = -1;
        elements[idx] = null;
        size--;

        // 删除最后一个元素无需处理
        if (hp <= size) {
            siftUp(hp);
            siftDown(hp);
        }
        return val;
    }

    @Override
    public T first() {
        if (size <= 0) {
            throw new IllegalStateException("Heap isEmpty!");
        }
        return elements[heap[1]];
    }

    @Override
    public int firstIndex() {
        if (size <= 0) {
            throw new IllegalStateException("Heap isEmpty!");
        }
        return heap[1];
    }

    @Override
    public boolean containsIndex(int idx) {
        if (idx < 0 || idx >= elements.length) {
            return false;
        }
        return idxMap[idx] != -1;
    }

    @Override
    public int size() {
        return size;
    }

    /**
     * 从上往下调整
     *
     * @param hp 当前节点,堆数组索引
     */
    private void siftDown(int hp) {
        int i = hp;
        while (i < size) {
            int m = i;
            int l = left(i);
            if (l <= size && prior(l, m)) {
                m = l;
            }
            int r = right(i);
            if (r <= size && prior(r, m)) {
                m = r;
            }
            if (m == i) {
                break;
            }
            swap(i, m);
            i = m;
        }
    }

    /**
     * 从下往上调整
     *
     * @param hp 当前节点,堆数组索引
     */
    private void siftUp(int hp) {
        int i = hp;
        while (i > 1) {
            int p = parent(i);
            if (p > 0 && prior(i, p)) {
                swap(i, p);
                i = p;
            } else {
                break;
            }
        }
    }

    /**
     * 父节点索引
     *
     * @param hp 当前节点,堆数组索引
     * @return 父节点索引
     */
    private int parent(int hp) {
        return hp / 2;
    }

    /**
     * 左子节点索引
     *
     * @param hp 当前节点,堆数组索引
     * @return 左子节点索引
     */
    private int left(int hp) {
        return 2 * hp;
    }

    /**
     * 右子节点索引
     *
     * @param hp 当前节点,堆数组索引
     * @return 右子节点索引
     */
    private int right(int hp) {
        return 2 * hp + 1;
    }

    /**
     * elements[heap[hp]] 是否优先于 elements[heap[hq]]
     *
     * @param hp 堆数组索引 hp
     * @param hq 堆数组索引 hq
     * @return true/false
     */
    private boolean prior(int hp, int hq) {
        return cmp.compare(elements[heap[hp]], elements[heap[hq]]) < 0;
    }

    /**
     * 交换 2 个元素的位置
     *
     * @param hp 堆数组索引 hp
     * @param hq 堆数组索引 hq
     */
    private void swap(int hp, int hq) {
        if (hp == hq) {
            return;
        }
        int t = heap[hp];
        heap[hp] = heap[hq];
        heap[hq] = t;
        idxMap[heap[hp]] = hp;
        idxMap[heap[hq]] = hq;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 1; i <= size; i++) {
            if (i > 1) {
                sb.append(", ");
            }
            sb.append("(")
                    .append(heap[i])
                    .append(", ")
                    .append(elements[heap[i]])
                    .append(")");
        }
        sb.append("]");
        return sb.toString();
    }

}