一文带你弄懂什么是索引二叉堆

引言

二叉堆的实现见:Java中的优先队列——二叉堆

索引二叉堆又称为最小索引优先队列。它的特点是堆元素位置不变

保存堆元素的数组不变,即不交换数组中任意元素的位置。利用一个额外的指向堆元素下标(或索引)的索引数组(pq)来代替它进行交换,同时为了操作方便,再维护一个pq的翻转数组(pq中的值作为新数组的索引,索引作为新数组的值)。

文字看起来有点懵就对了。我们直接看它的结构。

结构

在这里插入图片描述
假设我们按{4,2,3,1,6,5,7}的顺序插入,并且一旦插入结束之后,不会交换元素的位置。我们又想进行取堆顶元素等操作,怎么办?
可以新增一个特殊的数组(pq)来维护keys数组的索引,根据keys数组中元素的大小移动pq数组的位置。使得可以通过pq[1]得到最小元素。

pq维护的是keys数组的索引。因此pq[1]的值一定是keys数组中最小值的索引,上图中最小值为1,其索引为3。因此pq[1] = 3
那么取堆顶元素可以:keys[pq[1]]

当构建完成索引二叉堆后,pq的结构如下:

在这里插入图片描述
pq相当是二叉堆对应的数组,按惯例,该索引从1开始。不过,这个数组中的元素不是堆元素的值,而是堆元素在keys数组中的下标。

pqkeys的关系如下:

在这里插入图片描述
其实很简单,pq的值指向keys中对应的下标即可。因此,我们就可以根据keys中相应值的大小调整pq数组来达到使keys数组满足堆序性的目的。

索引二叉堆中还有一个特殊的翻转数组。翻转的就是pq,我们命名这个翻转数组为reversed
pq中的值作为翻转数组的索引,索引作为翻转数组的值。根据上面的例子,其翻转数组如下:

在这里插入图片描述
就是将pq中下标和其对应的值翻转了一下得到的数组。

为什么要这个翻转数组呢,其实它只是一个辅助数组,比如我们要得到keys数组索引5(keys数组的索引称为关联索引)在pq中对应的索引。那么就可以通过reversed[5]得到。该值为6,而pq[6] = 5

总结一下,它们之间满足这样一个等式:pq[reversed[i]] = reversed[pq[i]] = i 其中,ikeys数组的索引。

理解了这个结构,再去理解它的实现代码就不难了。

代码

package com.algorithms.heap;

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

/**
 * 索引二叉堆<p/>
 * 具有堆元素位置不变的性质
 * <p>
 * 保存堆元素的数组不变,即不交换数组中任意元素的位置。利用一个额外的指向堆元素下标的索引数组(pq)来代替它进行交换,
 * 同时为了操作方便,再维护一个pq的翻转数组(pq中的值作为新数组的索引,索引作为新数组的值)
 *
 * @author yjw
 * @date 2019/6/3/003
 */
@SuppressWarnings("unchecked")
public class IndexMinPQ<Key extends Comparable<? super Key>> implements Iterable<Integer> {
    /**
     * 堆中最大元素数量
     */
    private final int capacity;

    /**
     * 堆中元素数量
     */
    private int size = 0;

    /**
     * 指向堆元素引用的数组
     * 数组中的元素指向堆元素(keys)的索引
     * 数组里面只是索引,具体参与比较的还是对应的堆元素
     * <p>
     * 该数组满足堆性质,很容易知道最小元素为keys[pq[1]]
     * <p>
     * 索引范围: (0,size]
     */
    private int[] pq;

    /**
     * <strong>保存的是关联索引i -> pq中对应的索引,其下标就是关联索引</strong>
     * <p>
     * 以pq中值作为reversed的索引,pq值对应的索引作为reversed的值。
     * 相当于将pq数组值和下标翻转过来得到的数组。
     * <p>
     * 满足: reversed[pq[i]] = pq[reversed[i]] = i
     *
     * 索引范围: [0,size - 1)
     */
    private int[] reversed;

    /**
     * 堆元素数组,该数组不会进行交换操作
     * <p>
     * 索引范围: [0,size - 1)
     */
    private Key[] keys;

    public IndexMinPQ(int capacity) {
        if (capacity < 0) {
            throw new IllegalStateException();
        }
        this.capacity = capacity;
        keys = (Key[]) new Comparable[capacity + 1];
        pq = new int[capacity + 1];
        reversed = new int[capacity + 1];
        Arrays.fill(reversed, -1);//-1表示没有关联任何pq的索引
    }

    private void rangeCheck(int i) {
        if (i < 0 || i >= capacity) {
            throw new IndexOutOfBoundsException();
        }
    }

    /**
     * @param i 索引
     * @return 该索引是否在堆中
     */
    public boolean contains(int i) {
        rangeCheck(i);
        return reversed[i] != -1;
    }

    public int size() {
        return size;
    }

    /**
     * 后面所说的索引i(public 方法参数中的i)都指的是keys数组中的索引,也就是关联索引
     */

    /**
     * 关联元素e与索引i
     * <p>
     * 想查询i对应的元素,除了keys[i],还可以通过keys[pq[reversed[i]]]
     *
     * @param i
     * @param key
     */
    public void insert(int i, Key key) {
        if (contains(i)) {
            throw new IllegalStateException("index is already in the heap");
        }
        size++;
        keys[i] = key;
        pq[size] = i;//将索引添加到pq中最后的位置,保存新元素e在keys中的索引
        reversed[i] = size;//翻转pq
        swim(size);//上滤
    }

    /**
     * 删除最小的元素并返回它的关联索引
     *
     * @return
     */
    public int deleteMin() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        //pq下标为1的元素即keys中最小元素的关联索引,
        int min = pq[1];
        swap(1, size--);//用最后一个元素替代最小元素的位置
        sink(1);//下滤
        reversed[min] = -1;        // 标记为已删除
        keys[min] = null;    // 防止内存泄漏!!
        pq[size + 1] = -1;        // 标记之前的最后一个元素为已删除
        return min;
    }

    /**
     * 删除关联索引i处的元素,注意删除的是keys[i]对应的元素
     *
     * @param i
     */
    public void delete(int i) {
        if (!contains(i)) {
            throw new NoSuchElementException();
        }

        /**
         * 得到pq数组中的索引index,然后删除keys[i]处的元素
         */
        int index = reversed[i];
        /**
         * 同样用最后一个元素替代该元素
         */
        swap(index, size--);
        /**
         * 这里需要进行上滤和下滤操作
         * 有可能堆中最后的元素小于index处的父节点
         */
        swim(index);
        sink(index);
        keys[i] = null;//防止内存泄漏
        reversed[i] = -1;//表明关联索引i没有关联任何元素了
    }

    /**
     * 返回关联最小元素的索引
     *
     * @return
     */
    public int minIndex() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return pq[1];
    }

    public Key minKey() {
        return keys[minIndex()];
    }

    /**
     * 返回与索引i关联的元素
     *
     * @param i
     * @return
     */
    public Key keyOf(int i) {
        if (!contains(i)) {
            throw new NoSuchElementException();
        }
        return keys[i];
    }

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

    private boolean less(int i, int j) {
        return keys[pq[i]].compareTo(keys[pq[j]]) < 0;
    }

    /**
     * 将i关联的元素值增加到 key
     *
     * @param i
     * @param key 要满足大于i处的值
     */
    public void increaseKey(int i, Key key) {
        if (!contains(i)) {
            throw new NoSuchElementException();
        }
        if (keys[i].compareTo(key) >= 0) {
            throw new IllegalArgumentException("Calling increaseKey() with given argument would " +
                    "not strictly increase the key");
        }
        keys[i] = key;
        //因为是增大值,所以只需要下滤
        sink(reversed[i]);
    }

    public void decreaseKey(int i, Key key) {
        if (!contains(i)) {
            throw new NoSuchElementException();
        }
        if (keys[i].compareTo(key) <= 0) {
            throw new IllegalArgumentException("Calling decreaseKey() with given argument " +
                    "would not strictly decrease the key");
        }
        keys[i] = key;
        //上滤
        swim(reversed[i]);
    }

    public void changeKey(int i, Key key) {
        if (!contains(i)) {
            throw new NoSuchElementException();
        }
        keys[i] = key;
        //因为不知道是增加了还是减少了,所以需要在两个方向进行交换
        swim(reversed[i]);
        sink(reversed[i]);
    }

    /**
     * 另一种写法
     * @param i
     * @param key
     */
    /*public void changeKey(int i, Key key) {
        if (!contains(i)) {
            throw new NoSuchElementException();
        }
        if (keys[i].compareTo(key) < 0) {
            increaseKey(i, key);
        } else if (keys[i].compareTo(key) > 0) {
            decreaseKey(i, key);
        }
    }*/

    // swim/swap/sink都是对pq和reversed数组进行的,keys数组只能进行置null操作!!!

    /**
     * 上滤
     *
     * @param k
     */
    private void swim(int k) {
        /**
         * 当k比它的父节点要小时,上滤
         * 直到k不小于它的父节点
         * 或k变成了堆顶(k=1)
         */
        while (k > 1 && less(k, k / 2)) {
            swap(k, k / 2);
            k = k / 2;
        }
    }

    /**
     * 下滤
     *
     * @param k
     */
    private void sink(int k) {
        //如果有孩子,不停的下滤,直到满足堆的性质
        //k * 2 <= size 说明至少有左孩子
        while (k * 2 <= size) {
            int child = k * 2;
            //如果有右孩子(child +1),且右孩子更小
            if (child < size && less(child + 1, child)) {
                child = child + 1;
            }

            if (less(k, child)) {
                //如果小于孩子,就不用下滤了
                break;
            }
            swap(k, child);
            k = child;//更新k
        }
    }

    /**
     * 交换pq和reversed数组
     *
     * @param i
     * @param j
     */
    private void swap(int i, int j) {
        int tmp = pq[i];
        pq[i] = pq[j];
        pq[j] = tmp;
        reversed[pq[i]] = i;
        reversed[pq[j]] = j;
    }

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

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("capacity: ").append(capacity).append(",current size: ").append(size).append("\n");
        sb.append("keys:").append(Arrays.toString(keys)).append("\n");
        sb.append("pq:").append(Arrays.toString(pq)).append("\n");
        sb.append("reversed:").append(Arrays.toString(reversed));
        return sb.toString();
    }

    private class HeapIterator implements Iterator<Integer> {
        private IndexMinPQ<Key> copied;

        public HeapIterator() {
            copied = new IndexMinPQ<>(pq.length - 1);
            for (int i = 1; i <= size; i++) {
                //pq数组已经满足堆序性,因此没有元素会移动
                copied.insert(pq[i], keys[pq[i]]);
            }
        }

        @Override
        public boolean hasNext() {
            return !copied.isEmpty();
        }

        @Override
        public Integer next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            return copied.deleteMin();
        }
    }

    public static void main(String[] args) {
        Integer[] ints = {4, 2, 3, 1, 6, 5, 7};//假设以这个顺序插入堆

        IndexMinPQ<Integer> pq = new IndexMinPQ<>(ints.length);
        for (int i = 0; i < ints.length; i++) {
            pq.insert(i, ints[i]);
        }

        System.out.println(pq);
        // print each key using the iterator
        /*for (int i : pq) {
            System.out.println(i + " " + ints[i]);
        }*/
    }
}

发布了154 篇原创文章 · 获赞 216 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/yjw123456/article/details/105214581