|

索引堆及其优化详解:原理、图示与Java实现

索引堆及其优化详解:原理、图示与Java实现

引言

索引堆(Indexed Heap),也称为索引优先队列(Indexed Priority Queue),是一种扩展的堆数据结构。它在标准二进制堆的基础上,引入索引机制,允许高效地更新和删除任意元素,而不仅仅是堆顶元素。这在图算法(如Dijkstra最短路径算法)中特别有用,因为它能快速调整特定节点的优先级。 标准优先队列(如Java的PriorityQueue)不支持高效的任意元素更新,而索引堆通过额外映射解决了这个问题。

原理

索引堆的核心是结合二进制堆和索引映射:

  • 二进制堆基础:元素存储在数组中,形成完全二叉树。最小堆(Min-Heap)确保父节点小于或等于子节点;最大堆(Max-Heap)则相反。操作如插入(swim上浮)和删除(sink下沉)维持堆性质,时间复杂度为O(log n)。
  • 索引机制:使用两个数组实现双向映射:
  • pq[]:堆数组,存储索引(而非直接值),1-based indexing。
  • qp[]:逆映射数组,qp[i] 表示索引i在pq中的位置。
  • keys[]:存储实际键值,按索引访问。
  • 关键操作
  • 插入(insert):添加键到keys[i],更新pq和qp,然后上浮(swim)调整堆。
  • 删除最小(delMin):移除堆顶,交换末尾元素,下沉(sink)调整。
  • 更新键(changeKey/decreaseKey/increaseKey):直接修改keys[i],然后根据变化上浮或下沉。
  • 删除任意(delete):交换到末尾,移除,然后上浮和下沉调整。
  • 优势:所有操作均为O(log n),索引映射提供O(1)访问,避免线性扫描。

这种结构优化了标准堆的局限性,后者更新任意元素需O(n)时间查找。


优化

  • 时间复杂度优化:通过索引避免O(n)查找,更新操作从O(n + log n)降至O(log n)。在Dijkstra算法中,这可将总复杂度从O(V^2)优化到O((V+E) log V)。
  • 空间优化:使用数组而非链表,紧凑存储;qp[]确保快速定位。
  • 变体:可基于斐波那契堆(Fibonacci Heap)实现,摊销decreaseKey为O(1),但实现复杂,常用于理论优化。
  • 实际优化提示:在Java中,使用泛型确保类型安全;对于自定义键,重写compareTo或提供Comparator。避免频繁更新以防堆退化。

图示

以下是索引堆的典型图示,展示数组表示和树结构,以及操作过程。

Java实现

以下是基于Princeton Algorithms的完整Java实现(IndexMinPQ),这是一个最小索引堆。代码包括详细注释,支持泛型键。

import java.util.Iterator;
import java.util.NoSuchElementException;

public class IndexMinPQ<Key extends Comparable<Key>> implements Iterable<Integer> {
    private int maxN;        // maximum number of elements on PQ
    private int n;           // number of elements on PQ
    private int[] pq;        // binary heap using 1-based indexing
    private int[] qp;        // inverse of pq - qp[pq[i]] = pq[qp[i]] = i
    private Key[] keys;      // keys[i] = priority of i

    public IndexMinPQ(int maxN) {
        if (maxN < 0) throw new IllegalArgumentException();
        this.maxN = maxN;
        n = 0;
        keys = (Key[]) new Comparable[maxN + 1];
        pq   = new int[maxN + 1];
        qp   = new int[maxN + 1];
        for (int i = 0; i <= maxN; i++)
            qp[i] = -1;
    }

    public boolean isEmpty() {
        return n == 0;
    }

    public boolean contains(int i) {
        validateIndex(i);
        return qp[i] != -1;
    }

    public int size() {
        return n;
    }

    public void insert(int i, Key key) {
        validateIndex(i);
        if (contains(i)) throw new IllegalArgumentException("index is already in the priority queue");
        n++;
        qp[i] = n;
        pq[n] = i;
        keys[i] = key;
        swim(n);
    }

    public int minIndex() {
        if (n == 0) throw new NoSuchElementException("Priority queue underflow");
        return pq[1];
    }

    public Key minKey() {
        if (n == 0) throw new NoSuchElementException("Priority queue underflow");
        return keys[pq[1]];
    }

    public int delMin() {
        if (n == 0) throw new NoSuchElementException("Priority queue underflow");
        int min = pq[1];
        exch(1, n--);
        sink(1);
        qp[min] = -1;
        keys[min] = null;
        pq[n+1] = -1;
        return min;
    }

    public Key keyOf(int i) {
        validateIndex(i);
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        else return keys[i];
    }

    public void changeKey(int i, Key key) {
        validateIndex(i);
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        keys[i] = key;
        swim(qp[i]);
        sink(qp[i]);
    }

    public void decreaseKey(int i, Key key) {
        validateIndex(i);
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        if (keys[i].compareTo(key) == 0)
            throw new IllegalArgumentException("Calling decreaseKey() with a key equal to the key in the priority queue");
        if (keys[i].compareTo(key) < 0)
            throw new IllegalArgumentException("Calling decreaseKey() with a key strictly greater than the key in the priority queue");
        keys[i] = key;
        swim(qp[i]);
    }

    public void increaseKey(int i, Key key) {
        validateIndex(i);
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        if (keys[i].compareTo(key) == 0)
            throw new IllegalArgumentException("Calling increaseKey() with a key equal to the key in the priority queue");
        if (keys[i].compareTo(key) > 0)
            throw new IllegalArgumentException("Calling increaseKey() with a key strictly less than the key in the priority queue");
        keys[i] = key;
        sink(qp[i]);
    }

    public void delete(int i) {
        validateIndex(i);
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        int index = qp[i];
        exch(index, n--);
        swim(index);
        sink(index);
        keys[i] = null;
        qp[i] = -1;
    }

    private void validateIndex(int i) {
        if (i < 0) throw new IllegalArgumentException("index is negative: " + i);
        if (i >= maxN) throw new IllegalArgumentException("index >= capacity: " + 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;
        }
    }

    public Iterator<Integer> iterator() {
        return new HeapIterator();
    }

    private class HeapIterator implements Iterator<Integer> {
        // create a new pq
        private IndexMinPQ<Key> copy;

        // add all elements to copy of heap
        // takes linear time since already in heap order so no keys move
        public HeapIterator() {
            copy = new IndexMinPQ<Key>(pq.length - 1);
            for (int i = 1; i <= n; i++)
                copy.insert(pq[i], keys[pq[i]]);
        }

        public boolean hasNext()  { return !copy.isEmpty();                     }
        public void remove()      { throw new UnsupportedOperationException();  }

        public Integer next() {
            if (!hasNext()) throw new NoSuchElementException();
            return copy.delMin();
        }
    }
}

使用示例

public static void main(String[] args) {
    IndexMinPQ<String> pq = new IndexMinPQ<String>(10);
    pq.insert(0, "P");
    pq.insert(1, "Q");
    pq.insert(2, "E");
    pq.decreaseKey(2, "C");  // 更新键
    while (!pq.isEmpty()) {
        System.out.println(pq.minKey() + " " + pq.delMin());
    }
}

输出:C 2\nP 0\nQ 1

此实现可扩展为最大堆(IndexMaxPQ),只需调整比较逻辑。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注