图文堆排序

2023-07-15 ⏳1.6分钟(0.6千字)

图文理解堆排序.

缘起

堆排序咋一看有点反直觉, 这里详细记录下算法过程。

了解几个概念

  1. 完全二叉树
  2. 大堆/小堆

大堆就是父结点大于等于其子结点的值.

思路

考虑有个数组 [2, 1, 3] 我们想降序排列也就是变成 [3, 2, 1]。

我们先用完全二叉树的形式他构建成大堆, 如下:

大堆

很简单, 用代码也能简单的实现, 下面介绍下一个概念 heapify, 简单来说就是让三个结点最大的在父节点.

heapify

如何排序呢, 我们把最后的叶子结点 2 和root结点 3 交换, 然后最大值就在最后的叶子结点, 把最后一个节点摘掉, 那么取到了当前最大值, 但是当前的堆可能是乱掉的, 那么我们再次执行 heapify, 然后移除最后的叶子结点, 如此反复即可.

细节

了解了大致原理, 我们再来深入到细节。

heapify 函数

假设我们有一个数组[6, 7, 3, 2, 1, 9, 8, 5], 构造成二叉树.

数组下标

从图上我们可以看出, 当我们拿到数组的 index 可以轻松计算出其父结点子结点.

heapify 函数也很简单, 如下:

func heapify(nums []int, n int, i int) {
        largest := i
        l := 2*i + 1
        r := 2*i + 2
        if l < n && nums[l] > nums[largest] {
                largest = l
        }
        if r < n && nums[r] > nums[largest] {
                largest = r
        }
        if largest != i {
                nums[i], nums[largest] = nums[largest], nums[i]
                // 对子结点进行调整
                heapify(nums, n, largest)
        }
}

构建大堆

在一个简单的二叉树(只有三个结点的时候)构建成大堆我们已经处理过了, 怎么让一个复杂的二叉树构建成大堆呢?

逻辑上很简单, 我们只要从最后一个叶子结点的父结点往根结点方向一直执行 heapify 即可.

最后一个叶子结点的下标是 n-1, 那么根据上述公式其父节点为((n1)1)2\frac{((n-1)-1)}{2}n2\frac{n}{2} - 1.

func heapSort(nums []int) {
        n := len(nums)
        for i := n/2 - 1; i >= 0; i-- {
                heapify(nums, n, i)
        }
}

执行后数组从 [6, 7, 3, 2, 1, 9, 8, 5] 变为 [9 7 8 5 1 3 6 2].

构建大堆

final

上面我们成功构建了大堆, 那么现在我们只有每构建成大堆, 把root结点跟最后的叶子结点交换, 移除叶子结点(取出最大值), 然后再对剩下的元素进行heapify, 继续取最大值, 反复操作即可.

func heapSort(nums []int) {
        n := len(nums)
        for i := n/2 - 1; i >= 0; i-- {
                heapify(nums, n, i)
        }
        for i := n - 1; i >= 0; i-- {
                nums[0], nums[i] = nums[i], nums[0]
                heapify(nums, i, 0)
        }
}

总结

堆排序很容易忘记, 详细记一下.