亲宝软件园·资讯

展开

面试官说,你会堆排序吗?会,那好手写一个吧。

烟雨星空 人气:0
## 前言 最近明显文章更新频率降低了,那是因为我在恶补数据结构和算法的相关知识,相当于是从零开始学习。 找了很多视频和资料,最后发现 b 站尚硅谷的视频教程还是相对不错的,总共 195 集。每个小节都是按先概念、原理,然后代码实现的步骤讲解。如果你也准备入门数据结构和算法,我推荐可以看下这个系列教程。 ![](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172935721-799901927.jpg) 昨天一天一下子肝了 40 多集,从树的后半部分到图的全部部分。可以看到,每一集其实时间也不算长,短的几分钟,长的也就半个小时。开 2 倍速看,倍儿爽。 话不多说,下面进入正题。 ## 二叉堆介绍 我们知道,树有很多种,最常用的就是二叉树了。二叉树又有满二叉树和完全二叉树。而二叉堆,就是基于完全二叉树的一种数据结构。它有以下两个特性。 1. 首先它是一个完全二叉树 2. 其次,堆中的任意一个父节点的值都大于等于(或小于)它的左右孩子节点。 因此,根据第二个特性,就把二叉堆分为大顶堆(或叫最大堆),和小顶堆(或叫最小堆)。 顾名思义,大顶堆,就是父节点大于等于左右孩子节点的堆,小顶堆就是父节点小于左右孩子节点的堆。 看一下大顶堆的示例图,小顶堆类似,只不过是小值在上而已。 ![](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172936013-787574237.jpg) 注意:大顶堆只保证父节点大于左右孩子节点的值,不需要保证左右孩子节点之间的大小顺序。如图中,7 的左子节点 6 比右子节点 1 大,而 8 的左子节点 4 却比右子节点 5 小。(小顶堆同理) ## 构建二叉堆 二叉堆的定义我们知道了,那么给你一个无序的完全二叉树,怎么把它构建成二叉堆呢? 我们以大顶堆为例。给定以下一个数组,(完全二叉树一般用数组来存储) ``` {4, 1, 9, 3, 7, 8, 5, 6, 2} ``` 我们画出它的初始状态,然后分析怎么一步一步构建成大顶堆。 ![](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172936277-1960794746.jpg) 由于大顶堆,父节点的值都大于左右孩子节点,所以树的根节点肯定是所有节点中值最大的。因此,我们需要从树的最后一层开始,逐渐的把大值向上调整(左右孩子节点中较大的节点和父节点交换),直到第一层。 其实,更具体的说,应该是从下面的非叶子节点开始调整。想一想,为什么。 反向思考一下,如果从第一层开始调整的话,例如图中就是 4 和 9 交换位置之后,你不能保证 9 就是所有节点的最大值(额,图中的例子可能不是太好,正好是 9 最大)。如果下边还有比 9 大的数字,你最终还是需要从下面向上遍历调整。那么,我还不如一开始就直接从下向上调整呢。 另外,为什么从从最下面的非叶子节点(图中节点 3 )开始。因为叶子节点的下面已经没有子节点了,它只能和父节点比较,从叶子节点开始没有意义。 第一步,以 3 为父节点开始,比较他们的子节点 6和 2 ,6最大,然后和 3 交换位置。 ![①](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172936449-1291139845.jpg) 第二步,6 和 7 比较,7 最大,7 和 1 交换位置。 ![②](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172936660-364766474.jpg) 第三步,7 和 9 比较,9 最大,9 和 4 交换位置。 ![③](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172937015-526092225.jpg) 第四步,我们发现交换位置之后,4 下边还有比它大的,因此还需要以 4 为父节点和它的左右子节点进行比较。发现 8 最大,然后 8 和 4 交换位置。 ![④](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172937272-220542586.jpg) 最终,实现了一个大顶堆的构建。下面以代码实现交换过程。 ``` /** * 调整为大顶堆 * @param arr 待调整的数组 * @param parent 当前父节点的下标 * @param length 需要对多少个元素进行调整 */ private static void adjustHeap(int[] arr, int parent, int length){ //临时保存父节点 int temp = arr[parent]; //左子节点的下标 int child = 2 * parent + 1; //如果子节点的下标大于等于当前需要比较的元素个数,则结束循环 while(child < length){ //判断左子节点和右子节点的大小,若右边大,则把child定位到右边 if(child + 1 < length && arr[child] < arr[child + 1]){ child ++; } //若child大于父节点,则交换位置,否则退出循环 if(arr[child] > temp){ //父子节点交换位置 arr[parent] = arr[child]; //因为交换位置之后,不能保证当前的子节点是它子树的最大值,所以需要继续向下比较, //把当前子节点设置为下次循环的父节点,同时,找到它的左子节点,继续下次循环 parent = child; child = 2 * parent + 1; }else{ //如果当前子节点小于等于父节点,则说明此时的父节点已经是最大值了, //因此无需继续循环 break; } } //把当前节点值替换为最开始暂存的父节点值 arr[parent] = temp; } public static void main(String[] args) { int[] arr = {4,1,9,3,7,8,5,6,2}; //构建一个大顶堆,从最下面的非叶子节点开始向上遍历 for (int i = arr.length/2 - 1 ; i >= 0; i--) { adjustHeap(arr,i,arr.length); } System.out.println(Arrays.toString(arr)); } //打印结果: [9, 7, 8, 6, 1, 4, 5, 3, 2]。 和我们分析的结果一模一样 ``` 在 while 循环中,if(arr[child] > temp) else的逻辑, 对应的就是图中的第三步和第四步。即需要确保,交换后的子节点要比它下边的孩子节点都大,不然需要继续循环,调整位置。 ## 堆排序 堆排序就是利用大顶堆或者小顶堆的特性来进行排序的。 它的基本思想就是: 1. 把当前数组构建成一个大顶堆。 2. 此时,根节点肯定是所有节点中最大的值,让它和末尾元素交换位置,则最后一个元素就是最大值。 3. 把剩余的 n - 1个元素重新构建成一个大顶堆,就会得到 n-1 个元素中的最大值。重复执行此动作,就会把所有的元素调整为有序了。 **步骤:** 还是以上边的数组为例,看一下堆排序的过程。 一共有九个元素,把它调整为大顶堆,然后把堆顶元素 9 和末尾元素 2 交换位置。 ![](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172937508-1573407449.jpg) 此时,9已经有序了,不需要调整。然后把剩余八个元素调整为大顶堆,再把这八个元素的堆顶元素和末尾元素交换位置,如下,8 和 3 交换位置。 ![](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172937714-1968761437.jpg) 此时,8和 9 已经有序了,不需要调整。然后把剩余七个元素调整为大顶堆,再把这七个元素的堆顶元素和末尾元素交换位置。如下, 7 和 2 交换位置。 ![](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172937931-1219194028.jpg) 以此类推,经过 n - 1 次循环调整,到了最后只剩下一个元素的时候,就不需要再比较了,因为它已经是最小值了。 ![](https://img2020.cnblogs.com/other/1714084/202004/1714084-20200403172938141-525850602.jpg) 看起来好像过程很复杂,但其实是非常高效的。没有增删,直接在原来的数组上修改就可以。因为我们知道数组的增删是比较慢的,每次删除,插入元素,都要移动数组后边的 n 个元素。此外,也不占用额外的空间。 代码实现: ``` //堆排序,大顶堆,升序 private static void heapSort(int[] arr){ //构建一个大顶堆,从最下面的非叶子节点开始向上遍历 for (int i = arr.length/2 - 1 ; i >= 0; i--) { adjustHeap(arr,i,arr.length); } System.out.println(Arrays.toString(arr)); //循环执行以下操作:1.交换堆顶元素和末尾元素 2.重新调整为大顶堆 for (int i = arr.length - 1; i > 0; i--) { //将堆顶最大的元素与末尾元素互换,则数组中最后的元素变为最大值 int temp = arr[i]; arr[i] = arr[0]; arr[0] = temp; //从堆顶开始重新调整结构,使之成为大顶堆 // i代表当前数组需要调整的元素个数,是逐渐递减的 adjustHeap(arr,0,i); } } ``` **时间复杂度和空间复杂度:** 堆排序,每次调整为大顶堆的时间复杂度为 O(logn),而 n 个元素,总共需要循环调整 n-1 次 ,所以堆排序的时间复杂度就是 O(nlogn)。它的数学推导比较复杂,感兴趣的同学可以自己查看相关资料。 由于没有占用额外的内存空间,因此,堆排序的空间复杂度为 O(1)。

加载全部内容

相关教程
猜你喜欢
用户评论