算法之堆排序

194 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

引子

网上讲堆排序的挺多的,为什么还要赘述此篇,是因为每个人的关注点不一样,比如说堆排序对我来说,最大的难点,不是原理,而是每隔一段时间,再来回顾的时候,总是不能顺利写出代码,非要错个一两回才行。所以此篇根据自身的思考习惯,来理清一下需要掌握的知识点。

堆-这个数据结构

首先堆排序里的不是堆栈

之所以叫堆,可能是因为如果画出来这个数据结构的话,很像一个堆,就是一堆东西hu在一起的样子。想象成金字塔也是比较形象的。

这个数据结构包含多个元素,这些元素之间是有上下级的关系,实际上,就是一个树状结构,而且是二叉树。

但是堆这种二叉树,又有一些特殊性,比如说,这是一颗完全二叉树,什么叫完全二叉树呢?

我们把这颗二叉树画出来,层序遍历,从根到下,从左到右,一直遍历到最后一个元素,如果在这个过程中,没有缺失的节点,那么就叫完全二叉树,看文字不形象,看下图:

image.png

再来看看一个反例:

image.png

我们可以看见,染色的节点,缺失了子节点,所以最后一层遍历的时候,从左到右,不完整了,所以叫非完全二叉树

除了要是一颗完全二叉树之外,还要满足一个特点才能称之为

那就是每一个节点的值,都必须小于或等于左右两个子节点的值。(反之亦可)。

我们来看一个例子:

image.png

观察上图,

  • 首先它是完全二叉树;
  • 其次,节点的值 <=<= 子节点。

存储堆

虽然堆是一颗树,但是用数组也是可以来存储的。为什么?

这就是完全二叉树的好处。

就拿上图中的例子来说,层序遍历,从根到下,从左到右,把这些节点依次放到一个数组里,就存好了。

这样是不会丢失上下级关系的,从下标而言,假设数组里某元素的下标是i, 这个元素的上级节点的下标就是 i/21i/2 - 1。我们来画一下这个图:

image.png

反过来,下标是i的节点的

  • 左子节点的下标是 2i+12i + 1
  • 右子节点的下标是 2i+22i +2

当然,非要用树状结构来存储也是可以的, 并不需要非要用数组。

我们用数组来存,一般是为了搞堆排序。

堆排序之 heapify

现在有一个无序数组,肯定不是一颗合法的堆。

我们需要让这个数组变成一个合法的堆,这个操作叫 heapify,翻译叫 堆化。

down 操作

在 heapify 的过程中,又有一个小操作需要掌握,叫down操作。

必须条件:对于一个节点,如果左右节点所构成的树,都是合法的堆,而恰恰就是这个节点和左右两个子节点,不满足大小关系的时候,才可以对这个节点进行down操作。

我们来画一下这个必须条件:

image.png

我们发现,根节点不满足大小关系,但是根节点的左右两个子节点所构成的树,都满足堆的关系。

此时,对于根节点,才可以进行down操作,这个很重要。

down操作逻辑

我们根据上面的图来讲解,既然根节点不满足大小关系,很显然,我们需要将根节点往下降

  • 也就是在左右两个子节点中,选取一个,与根节点交换

那么选取哪一个呢?选取更小的那个就行,如果两个子节点同样大小,那么就随便选取一个,例如上图,两个子节点的值都是1,那么就随便选。

我们画出交换之后的图:

image.png

本来左边是好的,交换之后,左边又不行了,不怕,继续往下降就行了!一直降到满足为止。

image.png

image.png

heapify 逻辑

上面的例子中,如果之后根节点不满足的话,只需要对根节点进行down操作即可。

但现实是,一个无序的数组,肯定不仅仅是根节点不满足,而是所有节点都不满足。

思路就是,尽量一点点的让整个结构满足堆的关系。

我们从最后一个有子节点的节点开始,进行down操作,一直往前遍历,一直遍历到根节点,此时数组就完成了堆化。

这里之所以可行是因为,我们每次进行down操作的时候,确实已经满足了down操作的那个必须条件

image.png

堆排序具体逻辑

在完成heapify之后,就可以真正来进行堆排序了。

我们假设heapify的时候,采用的大小关系是,谁大,谁就往上升。

那么一个heapify之后的数组的第一个元素就是最大的。

此时,我们把第一个元素和最后一个元素互换。

我们会得到两个结果:

  • 最大的元素已经排到了最后,这就是排序所需要的
  • 忽略最后一个元素,前面的所有元素构成的是一个不那么完美的堆。因为根节点有可能不满足大小关系

此时我们只需要对根节点进行down操作,这次操作的时候,一定要忽略最后一个元素。

然后,就又得到一个好堆了。

画一个图就明白了:

image.png

图中白色部分是一个好堆。

在进行两个操作之后,数组的最后一个元素已经是最大的了,而且数组前面部分又是一个好堆。

我们一直进行这两个操作,最后得到的就是一个排好的数组,从小到大的顺序。

要反向排序的话,上面heapify的时候,只需要切换成小的在上面即可。

重要的操作总结

  • heapify 将整个树变成一个合法的堆,具体过程就是从尾部往前遍历每个节点,进行down操作即可。(优化成,从尾部第一个拥有子节点的元素开始)
  • down 如果一个节点和两个子节点不满足大小关系,那么就将这个节点往下降,一直往下降,降到满足即可。