本文一起来研究个常见算法,但是你不一定会 。

什么是小顶堆

小顶堆是一种经过排序的完全二叉树, 其满足如下性质:

  1. 小顶堆中的任意父节点都比其两个孩子结点小

由上方性质又可以推导出如下性质:

  1. 小顶堆的根节点为整个堆元素中最小的元素
将小顶堆装入数组

我们当然可以用面向对象的方式描述一颗二叉树, 但是有没有不浪费一丁点空间. 即除了元素本身开销外, 尽量不额外占用内存空间的描述方式呢?

有的, 我们可以把小顶堆装入数组中. 为了把小顶堆装入数组中, 我们需要给出一个数组中的元素, 就能计算出其对应的父结点, 以及其两个子节点对应的位置 (这样我们就能定位到小顶堆中所有元素的位置, 可以操作任意一个元素, 这样就达到用数组描述小顶堆的目的啦).

装入数组的小顶堆元素位置满足如下性质:

  1. 假设有结点m在数组中下标为n, 那么其左孩子结点下标为2n+1, 右孩子结点下标为2n+2
三条性质, 条条有用

通过上面的介绍, 我们现在知道一个装在数组中的小顶堆要满足如下三个性质:

  1. 小顶堆中的任意父节点都比其两个孩子结点小
  2. 小顶堆的根节点为整个堆元素中最小的元素
  3. 假设有结点m在数组中下标为n, 那么其左孩子结点下标为2n+1, 右孩子结点下标为2n+2

这三个性质第一点是元素构成小顶堆的基本性质, 衍生出的第二点性质是利用小顶堆对无序元素进行堆排序的关键, 第三点性质是为了将小顶堆装入到数组中.

来一根数组, 以及一打元素下酒

假设我们有一个长度为10的无序数组:

[5, 2, 1, 9, 6, 7, 3, 4, 0, 8]

 

按照性质3我们可以将无序数组等价还原成一颗完全二叉树:

  •  
      5     / \    2   1  / \   / \  9  6 7 3 / \   /4  0 8

我们的目标是将其元素排序, 构造成符合性质1和2的完全二叉树, 即小顶堆(同一个完全二叉树排序后小顶堆有多种情况, 下方展示其中一种) 。

排序后构造成小顶堆的数组:[0, 2, 1, 4, 6, 7, 3, 5, 9, 8]

 

也就是下方所示的小顶堆:

  •  
      0     / \    2   1  / \   / \  4  6 7 3 / \   /5  9  3
排序! 每个元素都有属于自己的位置

以下方的完全二叉树为例, 我们分析一下, 如何调整元素顺序, 来将其构造成小顶堆呢.

  •  
      5     / \    2   1  / \   / \  9  6 7 3 / \   /4  0 8

根据性质1, 我们要让所有的父节点都比其直接子节点小, 也就是说我们要把二叉树中每一个子树的父节点与其子节点比较, 如果父节点比子节点大, 那么将父节点与其交换, 使得子树满足最小堆的性质.

那么父节点要与孩子结点比较几次呢, 是存在几个孩子就比较几次吗, 其实比较一次就可以, 我们先让两个孩子结点比较, 找到较大的一个, 然后让父节点与较大的比较, 如果父节点比较大的子节点大, 那么它肯定也比较小的子节点大, 将父节点与较大的子节点交换, 那么此时子树就满足最小堆性质.

将整颗二叉树, 最后一个拥有子节点的父节点进行上述调整, 当从后往前处理完所有节点后, 整颗二叉树都满足最小堆性质, 那么就完成了最小堆的构建. 整个过程看起来像是元素在下沉, 比如根节点5, 最终下沉到了4的位置.

上代码

接下来的代码展示了如何构建小顶堆, 依赖了lombok与junit, 放在IDE里调试运行可以理解的更清晰.

 

/**
 * 最小堆的定义是父节点一定比其两个直接子节点要大
 * 根节点一定是所有元素中最小的, 依据这个性质可以从前往后不断构建小顶堆, 最终整个数组元素升序排列, 这就是堆排序
 *
 * @author Gaozl
 * @date 2020/6/10 17:00
 */
@Slf4j
public class MinHeap {

    /**
     * 元素数组, 需要先了解下如何用数组表示二叉树
     * 数组表示的二叉树有如下特性
     * 假设有结点m下标为n, 那么2n+1是结点m的左孩子, 2n+2是结点m的右孩子
     */
    private Integer[] elements = {5, 2, 1, 9, 6, 7, 3, 4, 0, 8};

    /**
     * 尝试将给定元素放到指定下标, 如果不符合小顶堆特性将调整结点位置直到符合
     * @param index 元素尝试放入的下标
     * @param element 给定元素
     * @param length 用数组前几个元素构建小顶堆
     */
    public void siftDown(int index, Integer element, int length) {
        int half = length >>> 1;
        while (index < half) {
            int leftChild = 2 * index + 1;
            int rightChild = 2 * index + 2;
            // 比左右两个孩子中较小的孩子大才需要下沉, 因为比较小孩子小时肯定也比较大的孩子小, 此时比两个孩子都小无需下沉
            int compareChild = (rightChild < length) // 存在rightChild
                    && elements[rightChild] < elements[leftChild] ? rightChild : leftChild;
            // 当前元素比孩子大, 需要下沉
            if (element > elements[compareChild]) {
                elements[index] = elements[compareChild];
                index = compareChild;
            } else { // 否则无需下沉, 停止处理
                break;
            }
        }
        elements[index] = element;
    }

    /**
     * 将数组构建成小顶堆
     */
    public void heapify(int length) {
        for (int i = (length >>> 1) - 1; i >= 0; i--) { // 遍历所有非叶子结点
            siftDown(i, elements[i], length); // 将每个非叶子结点调整到符合小顶堆性质后, 整个数组自然构建成了小顶堆
        }
    }

    /**
     * 用数组构建小顶堆
     */
    @Test
    public void testHeapify() {
        log.info("{}", toString());
        heapify(elements.length);
        log.info("{}", toString());
    }

    @Override
    public String toString() {
        return Arrays.toString(elements);
    }
}
堆排序

既然我们都了解了如何构建小顶堆, 那么可以进一步了解一下很有意思的排序方法, 堆排序. 堆排序依赖堆的第2条性质. 直接上代码吧:)

/**
* 堆排序, 降序是用小顶堆, 小顶堆每次找到最小的元素并下沉.
* 升序是用大顶堆, 大顶堆每次找到最大元素并下沉
*/
@Test
public void testDescendSort() {
    log.info("{}", toString());
    // 先将数组所有元素构建小顶堆, 然后将堆顶最小元素与小顶堆最后一个元素交换
    // 排除此时最后一个最小的元素, 前面元素继续构建小顶堆, 找到最小元素继续交换到此时小顶堆最后一个元素
    // 经过构建length次小顶堆, 此时数组里所有元素已经是降序排列
    for (int i = 0; i < elements.length; i++) {
        int length = elements.length - i;
        heapify(length);
        int temp = elements[length - 1];
        elements[length - 1] = elements[0];
        elements[0] = temp;
    }
    log.info("{}", toString());
}
拓展

本篇文章的构建小顶堆代码并不是我凭空想象出来的, 是JDK1.8中优先队列PriorityQueue类中的代码片段改造注释而来. 既然聪明的你已经读到了这里, 那么可以顺便去看看PriorityQueue源代码实现啦, 如果还是看不懂也没关系, 请参考如下开源项目, 超级方便哦, 一个致力于帮大家节约阅读JDK源码时间的开源项目 。

https://github.com/gaozhilai/open-jdk1.8-analysis

 

面试官:你会手撕小顶堆算法排序吗?_算法