二叉堆的学习笔记

206 阅读5分钟

二叉堆

来自算法(第四版)优先队列堆实现的笔记,大部分图片皆来源于此

当一棵二叉树的每个结点都大于等于它的两个子结点时,它被称为堆有序。 根结点是堆有序的二叉树中的最大结点

如图所示:

在一个堆中,位置 k 的结点的父结点的位置为[k/2],而它的两个子结点的位置则分别为 2k2 k+1

从 a[k] 向上一层,就令 k 等于 k/2 ,向下一层则令 k 等于 2k 或 2k+1

堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态恢复,叫做堆的有序化

有序化的两种情况:

  1. 当某个结点的优先级上升(或是在堆底加入一个新的元素)时,我们需要由下至上恢复堆的顺序
  2. 当某个结点的优先级下降(例如,将根节点或父节点替换为一个较小的元素)时,我们需要由上至下恢复堆的顺序

由下至上的堆有序化(上浮)

因为某个结点变得比它的父结点更大而被打破,那么我们就需要通过交换它和它的父结点来修复堆 进行交换后,这个结点比它的两个子节点都大,但这个结点仍然可能比它现在的父节点更大,就继续重复的操作,直到恢复秩序。 因为k结点的父节点位置为k/2,如果k结点大于它的父节点那么堆的有序状态才会被打破,因此只要该结点不大于它的父节点,堆的有序状态就恢复了

代码实现

    private void swim(int k) {
        while (k > 1 && less(k / 2, k)) {
        // 当k结点大于父节点k/2时,堆的有序状态被打破,则交换他们之间的位置
            exch(k / 2, k);
            k = k / 2;
        }
    }

图示

由上至下的堆有序化(下沉)

因为某个结点变得比它的两个子节点或是其中之一更小而被打破,那么我们就需要通过将它和它的两个子节点中较大者交换来恢复堆,直到该结点的子节点都比它更小或者到达了堆底。

代码实现

private void sink(int k) {
        while (2 * k <= N) {
            // 获取该结点的子节点之一
            int j = 2 * k;
            if (j < N && less(j, j + 1)) {
                // 获取子节点中较大者
                j++;
            }
            if (!less(k, j)) {
                // 如果子节点已经比结点更小,那么堆已经有序化,无需进行下面的交换操作
                break;
            }
            // 交换结点与子节点较大者的位置
            exch(k, j);
            k = j;
        }
    }

图示

实现优先队列(基于最大堆)

API

MaxPQ<Key extends Comparable<Key>>

返回 方法名 描述
void insert(Key v) 想优先队列插入一个元素
Key max() 返回当前最大的元素(根节点)
Key delMax() 删除并返回最大元素
boolean isEmpty() 返回队列是否为空
int size() 返回队列当前元素个数
MaxPQ() 创建一个优先队列

时间复杂度

方法名 时间复杂度
insert(Key v) logN
delMax() logN

插入元素

将新元素添加到数组尾部,增加堆大小并让这个新元素上浮到合适的位置

图示

代码实现

    public void insert(Key v) {
        pq[++N] = v;
        swim(N);
    }

删除最大元素

删除数组顶端最大的元素,并与数组的最后一个元素交换,减小堆大小并进行下沉操作

图示

代码实现

    public Key delMax() {
        // 获取根节点的最大元素
        Key max = pq[1];
        // 与最后一个结点交换
        exch(1, N);
        // 防止对象游离
        pq[N--] = null;
        // 下沉
        sink(1);
        return max;
    }

完整代码实现

public class MaxPQ<Key extends Comparable<Key>> {
    private static final int DEFAULT_CAPACITY = 10;
    // pq[1..N]
    private Key[] pq;
    private int N;

    /**
     * 创建一个优先队列
     */
    public MaxPQ() {
        pq = (Key[]) new Comparable[DEFAULT_CAPACITY + 1];
    }
    
    /**
     * @return 根节点
     */
    public Key max() {
        return pq[1];
    }
    
    /**
     * 删除根节点的元素,并与数组的最后一个元素交换,减小堆大小并进行下沉操作
     *
     * @return 删除并返回最大元素
     */
    public Key delMax() {
        // 获取根节点
        Key max = pq[1];
        // 与最后一个结点交换
        exch(1, N);
        // 防止对象游离
        pq[N--] = null;
        // 下沉
        sink(1);
        return max;
    }
    
    /**
     * @return 返回队列是否为空
     */
    public boolean isEmpty() {
        return N == 0;
    }
    
    /**
     * @return 返回优先队列中的元素个数
     */
    public int size() {
        return N;
    }

    private boolean less(int i, int j) {
        return pq[i].compareTo(pq[j]) < 0;
    }
    
    private void exch(int i, int j) {
        Key t = pq[i];
        pq[i] = pq[j];
        pq[j] = t;
    }
    
    /**
     * 将新元素添加到数组尾部,增加堆大小并让这个新元素上浮到合适的位置
     *
     * @param v
     */
    public void insert(Key v) {
        pq[++N] = v;
        swim(N);
    }
    
    /**
     * 由下至上的堆有序化(上浮)
     *
     * @param k 结点
     */
    private void swim(int k) {
        while (k > 1 && less(k / 2, k)) {
            exch(k / 2, k);
            k /= 2;
        }
    }
    
    /**
     * 由下至上的堆有序化(下沉)
     *
     * @param k 结点
     */
    private void sink(int k) {
        while (2 * k <= N) {
            // 获取k结点的子节点之一
            int j = 2 * k;
            if (j < N && less(j, j + 1)) {
                // 获取子节点中较大者
                j++;
            }
            if (!less(k, j)) {
            // 如果子节点已经比结点更小,那么堆已经有序化,无需进行下面的交换操作
                break;
            }
            // 交换结点与子节点较大者的位置
            exch(k, j);
            k = j;
        }
    }
}