图解索引二叉堆

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

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

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

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

结构

图解索引二叉堆_第1张图片
假设我们按{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的关系如下:

图解索引二叉堆_第2张图片
其实很简单,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;

/**
 * 索引二叉堆

* 具有堆元素位置不变的性质 *

* 保存堆元素的数组不变,即不交换数组中任意元素的位置。利用一个额外的指向堆元素下标的索引数组(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)的索引 * 数组里面只是索引,具体参与比较的还是对应的堆元素 *

* 该数组满足堆性质,很容易知道最小元素为keys[pq[1]] *

* 索引范围: (0,size] */ private int[] pq; /** * 保存的是关联索引i -> pq中对应的索引,其下标就是关联索引 *

* 以pq中值作为reversed的索引,pq值对应的索引作为reversed的值。 * 相当于将pq数组值和下标翻转过来得到的数组。 *

* 满足: reversed[pq[i]] = pq[reversed[i]] = i * * 索引范围: [0,size - 1) */ private int[] reversed; /** * 堆元素数组,该数组不会进行交换操作 *

* 索引范围: [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 *

* 想查询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]); }*/ } }

你可能感兴趣的:(java,数据结构与算法)