示例练习>>>

可以把堆看做是一棵完全二叉树。堆一般分为两种,一种是最大堆(有的也叫大根堆,大顶堆),一种是最小堆。最大堆根节点的值是堆中最大的,最小堆根节点的值是堆中最小的。两者原理差不多,这里只介绍其中一种,我们就拿最小堆来讲解。因为堆是一棵完全二叉树,所以如果我们知道子节点的下标,那么一定知道父节点的下标,如果知道父节点的下标也一定知道子节点的下标(假设有子节点),如下图所示。

他们的关系如下:

父节点的下标 = (子节点下标-1) >> 1;
左子节点下标 = 父节点下标*2 + 1;
右子节点下标 = 父节点下标*2 + 2;

堆中添加元素

堆的常见操作有两个,一个是往堆中添加元素,一个是删除堆顶元素。往堆中添加元素实际上相当于在完全二叉树中添加一个叶子节点,添加完之后还需要往上调整,就是和父节点比较谁的值小(这里介绍的是最小堆),如果比父节点小就和父节点交换,交换完之后,还要继续往上比较 …… ,如果比父节点值大就不在交换。如下图所示。

这样通过不断的和父节点比较,如果添加的元素是堆中最小的,他就会跑到根节点,也就是最小堆的根节点是堆中所有元素中最小的。如果添加元素不比父节点小,就不需要交换。

堆中删除元素

堆中元素的添加只需要和父节点比较即可,因为一个节点最多只能有一个父节点(根节点没有父节点)。但堆的删除就点麻烦了,因为如果直接删除,就会把一棵二叉树变成两棵二叉树。实际上堆的删除并不是直接删除,而是让最后一个叶子节点(二叉树最下面一行最右边的节点)替换根节点,然后根节点往下调整,往下调整的步骤就是:

  • 如果根节点比他的两个子节点都小,就不需要往下调整。
  • 如果其中的一个子节点比根节点小,根节点就和那个比他小的子节点交换,交换完之后继续往下比较。
  • 如果根节点比他的两个子节点都大,根节点就和那个最小的子节点交换,交换完之后继续往下比较。

堆的添加会往上调整,堆的删除会往下调整,这里的删除只删除堆顶的元素,如果不是堆顶元素,两个方向都要考虑,我们来看下代码。

public class MyHeap<E> {
    private Object[] data;// 数据存放区。
    private int size;// 堆的大小。
    private Comparator<? super E> comparator;// 比较器。

    public MyHeap(int initialCapacity) {
        this(initialCapacity, null);
    }

    public MyHeap(int initialCapacity, Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException("堆的大小必须大于0");
        this.data = new Object[initialCapacity];
        this.comparator = comparator;
    }

    // 向堆中添加元素。
    public boolean add(E e) {
        if (e == null)// 不能为空。
            throw new NullPointerException();
        if (size >= data.length)// 如果堆的空间不够了就扩容,这里是扩大2倍。
            data = Arrays.copyOf(data, data.length << 1);
        if (size == 0)// 如果堆是空的,直接添加就可以了,不需要调整。
            data[0] = e;
        else// 如果堆不是空的,就要往上调整。。
            siftUp(e);
        size++;// 添加完之后size要加1。
        return true;
    }

    public int getSize() {
        return size;
    }

    // 删除堆顶元素。
    public E remove() {
        if (size == 0)
            return null;
        size--;
        E result = (E) data[0];// 获取堆顶的元素。
        E x = (E) data[size];// 取出数组的最后一个元素。
        data[size] = null;// 然后把最后一个元素的位置置空。
        // 这里实际上是把数组的最后一个元素取出放到堆顶,然后在往下调整。
        if (size != 0)
            siftDown(x);
        return result;
    }

    // 获取堆顶元素,不删除。
    public E peek() {
        return (size == 0) ? null : (E) data[0];
    }

    // 往上调整,往上调整只需要和父节点比较即可,如果比父节点大就不需要在调整。
    private void siftUp(E e) {
        int s = size;
        while (s > 0) {
            int parent = (s - 1) >>> 1;// 父节点的位置。
            Object pData = data[parent];
            // 和父节点比较,如果比父节点大就退出循环不在调整。
            if (comparator != null) {
                if (comparator.compare(e, (E) pData) >= 0)
                    break;
            } else {
                if (((Comparable<? super E>) e).compareTo((E) pData) >= 0)
                    break;
            }
            // 如果比父节点小,就和父节点交换,然后在继续往上调整。
            data[s] = pData;
            s = parent;
        }
        // 通过上面的往上调整,找到合适的位置,再把e放进去。
        data[s] = e;
    }

    // 往下调整,往下调整需要和他的两个子节点(如果有两个子节点)都要比较,
    // 哪个最小就和哪个交换,如果比两个子节点都小就不用在交换。
    private void siftDown(E e) {
        int half = size >>> 1;
        int index = 0;// 从根节点开始往下调整。
        while (index < half) {
            int min = (index << 1) + 1;// 左子节点的位置。
            Object minChild = data[min];
            int right = min + 1;// 右子节点的位置。
            if (right < size) {
                // 如果有右子节点,肯定会有左子节点。左右两个子节点比较取最小值。
                if (comparator != null) {
                    if (comparator.compare((E) minChild, (E) data[right]) > 0)
                        minChild = data[min = right];
                } else {
                    if (((Comparable<? super E>) minChild).compareTo((E) data[right]) > 0) {
                        minChild = data[min = right];
                    }
                }
            }
            // 节点e和他的最小的子节点比较,如果小于他最小的子节点就退出循环,
            // 不再往下调整了,
            if (comparator != null) {
                if (comparator.compare(e, (E) minChild) <= 0)
                    break;
            } else {
                if (((Comparable<? super E>) e).compareTo((E) minChild) <= 0)
                    break;
            }
            // 如果e比它的最小的子节点小,就用最小的子节点和e交换位置,然后再继续往下调整。
            data[index] = minChild;
            index = min;
        }
        data[index] = e;
    }
}

参考:
PriorityQueue.java

相关链接

数组
数组滚动数组差分数组树状数组
链表
链单向表双向链表循环链表跳表异或链表
队列
队列循环队列双端队列
散列表
散列表
二叉树二叉搜索树AVL树红黑树字典树哈夫曼树线段树笛卡尔树
图的介绍图的遍历Dijkstra算法Bellman-Ford算法SPFA算法Floyd算法Prim算法Kruskal算法Boruvka算法拓扑排序

信奥赛编程(刷题请进)>>>

经过两年的打磨,我的新作《算法秘籍》已经出版,有需要的可以点击购买。也可以点击 内容介绍 查看详情。