前言最近明显文章更新频率降低了,那是因为我在恶补数据结构和算法的相关知识,相当于是从零开始学习。 找了很多视频和资料,最后发现 b 站尚硅谷的视频教程还是相对不错的,总共 195 集。每个小节都是按先概念、原理,然后代码实现的步骤讲解。如果你也准备入门数据结构和算法,我推荐可以看下这个系列教程。 昨天一天一下子肝了 40 多集,从树的后半部分到图的全部部分。可以看到,每一集其实时间也不算长,短的几分钟,长的也就半个小时。开 2 倍速看,倍儿爽。 话不多说,下面进入正题。 二叉堆介绍我们知道,树有很多种,最常用的就是二叉树了。二叉树又有满二叉树和完全二叉树。而二叉堆,就是基于完全二叉树的一种数据结构。它有以下两个特性。
因此,根据第二个特性,就把二叉堆分为大顶堆(或叫最大堆),和小顶堆(或叫最小堆)。 顾名思义,大顶堆,就是父节点大于等于左右孩子节点的堆,小顶堆就是父节点小于左右孩子节点的堆。 看一下大顶堆的示例图,小顶堆类似,只不过是小值在上而已。 注意:大顶堆只保证父节点大于左右孩子节点的值,不需要保证左右孩子节点之间的大小顺序。如图中,7 的左子节点 6 比右子节点 1 大,而 8 的左子节点 4 却比右子节点 5 小。(小顶堆同理) 构建二叉堆二叉堆的定义我们知道了,那么给你一个无序的完全二叉树,怎么把它构建成二叉堆呢? 我们以大顶堆为例。给定以下一个数组,(完全二叉树一般用数组来存储)
我们画出它的初始状态,然后分析怎么一步一步构建成大顶堆。 由于大顶堆,父节点的值都大于左右孩子节点,所以树的根节点肯定是所有节点中值最大的。因此,我们需要从树的最后一层开始,逐渐的把大值向上调整(左右孩子节点中较大的节点和父节点交换),直到第一层。 其实,更具体的说,应该是从下面的非叶子节点开始调整。想一想,为什么。 反向思考一下,如果从第一层开始调整的话,例如图中就是 4 和 9 交换位置之后,你不能保证 9 就是所有节点的最大值(额,图中的例子可能不是太好,正好是 9 最大)。如果下边还有比 9 大的数字,你最终还是需要从下面向上遍历调整。那么,我还不如一开始就直接从下向上调整呢。 另外,为什么从从最下面的非叶子节点(图中节点 3 )开始。因为叶子节点的下面已经没有子节点了,它只能和父节点比较,从叶子节点开始没有意义。 第一步,以 3 为父节点开始,比较他们的子节点 6和 2 ,6最大,然后和 3 交换位置。 第二步,6 和 7 比较,7 最大,7 和 1 交换位置。 第三步,7 和 9 比较,9 最大,9 和 4 交换位置。 第四步,我们发现交换位置之后,4 下边还有比它大的,因此还需要以 4 为父节点和它的左右子节点进行比较。发现 8 最大,然后 8 和 4 交换位置。 最终,实现了一个大顶堆的构建。下面以代码实现交换过程。
在 while 循环中,if(arr[child] > temp) else的逻辑, 对应的就是图中的第三步和第四步。即需要确保,交换后的子节点要比它下边的孩子节点都大,不然需要继续循环,调整位置。 堆排序堆排序就是利用大顶堆或者小顶堆的特性来进行排序的。 它的基本思想就是:
步骤: 还是以上边的数组为例,看一下堆排序的过程。 一共有九个元素,把它调整为大顶堆,然后把堆顶元素 9 和末尾元素 2 交换位置。 此时,9已经有序了,不需要调整。然后把剩余八个元素调整为大顶堆,再把这八个元素的堆顶元素和末尾元素交换位置,如下,8 和 3 交换位置。 此时,8和 9 已经有序了,不需要调整。然后把剩余七个元素调整为大顶堆,再把这七个元素的堆顶元素和末尾元素交换位置。如下, 7 和 2 交换位置。 以此类推,经过 n - 1 次循环调整,到了最后只剩下一个元素的时候,就不需要再比较了,因为它已经是最小值了。 看起来好像过程很复杂,但其实是非常高效的。没有增删,直接在原来的数组上修改就可以。因为我们知道数组的增删是比较慢的,每次删除,插入元素,都要移动数组后边的 n 个元素。此外,也不占用额外的空间。 代码实现:
时间复杂度和空间复杂度: 堆排序,每次调整为大顶堆的时间复杂度为 O(logn),而 n 个元素,总共需要循环调整 n-1 次 ,所以堆排序的时间复杂度就是 O(nlogn)。它的数学推导比较复杂,感兴趣的同学可以自己查看相关资料。 由于没有占用额外的内存空间,因此,堆排序的空间复杂度为 O(1)。 |
|