排序算法相必大家都见过很多种,例如快速排序、归并排序、冒泡排序等等。今天,我们就来简单讲讲堆排序。
在上一篇中,我们讲解了二叉堆,今天的堆排序算法主要就是依赖于二叉堆来完成的,不清楚二叉堆是什么鬼的,可以看下:
【算法与数据结构】二叉堆是什么鬼?
用辅助数组来实现堆排序算法
假如给你一个二叉堆,根据二叉堆的特性,你会怎么使用二叉堆来实现堆排序呢?
我们都知道,二叉堆有一个很特殊的节点 --- 堆顶,堆顶要嘛是所有节点的最大元素,要嘛是最小元素,这主要取决于这个二叉堆是最小堆还是最大堆。
今天,我们暂且选择以最小堆来作为例子。
基于堆顶这个特点,我们就可以来实现我们的堆排序了。
大家看下面一个例子:
对于一个如图有10个节点元素的二叉堆:
![334244da2e0aa76c465c6baa9082fd7e.png](https://i-blog.csdnimg.cn/blog_migrate/d8d4675ce4ca2ba90781ca54631c749a.jpeg)
我们把堆顶这个节点删除,然后把删除的节点放在一个辅助数组help里。
![614304bc6f77a6e1050505af0ddb6471.png](https://i-blog.csdnimg.cn/blog_migrate/98cd5d67d503bfb6175253bd781a6497.jpeg)
显然,这个被删除的节点,是堆中最小的节点。接下来,我们继续删除二叉堆的堆顶,然后把删除的元素还是存放在help数组里。
![f1470540c03f89a7ae83df183a98fbaa.png](https://i-blog.csdnimg.cn/blog_migrate/4e9c48ac4a77c5c5309f15e7c79d29ed.jpeg)
显然,第二次删除的节点,是原始二叉堆中的第二小节点。
继续删除
![5eb0fb6c2c9f2e162e49cc231c9898bc.png](https://i-blog.csdnimg.cn/blog_migrate/3d6929cc04e97b0b946170658ac3f335.jpeg)
继续连续6次删除堆顶,把删除的节点一次放入help数组。
![eb666d45610c5d5fe86ff40d80cd6383.png](https://i-blog.csdnimg.cn/blog_migrate/3a6d2eb42a34ae1c273089555abf9451.jpeg)
二叉堆中只剩最后一个节点了,这个节点同时也是原始二叉堆中的最大节点,把这个节点继续删除了,还是放入help数组里。
![2f46dd6aea89b5074b0bdf4ad9e5200f.png](https://i-blog.csdnimg.cn/blog_migrate/8b607f834de7dc9b2062d8ae08a7bbe9.jpeg)
此时,二叉堆的元素被删除光了,观察一下help数组。这是一个有序的数组,实际上,通过从二叉堆的堆顶逐个取出最小值,存放在另一个辅助的数组里,当二叉堆被取光之时,我们就完成了一次堆排序了。
其实无需辅助数组
在上面的堆排序过程中,我们使用了一个辅助数组help。可事实上,我们真的需要辅助数组吗?
上篇讲二叉堆的时候,我们说过。二叉堆在实现的时候,是采取数组的形式来存储的。
从二叉堆中删除一个元素,为了充分利用空间,其实我们是可以把删除的元素直接存放在二叉堆的最后一个元素那里的。例如:
![255e5b5c1cd929eeab99ec54945c3d43.png](https://i-blog.csdnimg.cn/blog_migrate/cd66efcc1fb826b91583f9cf4bde58c0.jpeg)
删除堆顶,把删除的元素放在最后一个元素。
![bfdc9215ba90e97a3f8808433f33dbb8.png](https://i-blog.csdnimg.cn/blog_migrate/e1b2f7273bf161d22c78868fa0280fce.jpeg)
继续删除,把删除的元素放在最后第二个位置
![41cf98afb7665d2cdd6285ced0d16259.png](https://i-blog.csdnimg.cn/blog_migrate/dd386becd8e0e88e8025375a5edeebc9.jpeg)
继续删除,把删除的元素放在最后第三个位置
![3f0e0f517040132fe95387c18dfba61a.png](https://i-blog.csdnimg.cn/blog_migrate/1d2fea08c20412d1f4e2d5c770ac5da0.jpeg)
以此类推….
![503e144d3fa57c45a8a3c2122ffcc529.png](https://i-blog.csdnimg.cn/blog_migrate/ae8a25a5c2fe0e8a45768e8031ca66f1.jpeg)
这样,对于一个含有n个元素的二叉堆,经过n-1(不用删除n次)次删除之后,这个数组就是一个有序数组了。
![f83761822020caa72dee9b032c7e386d.png](https://i-blog.csdnimg.cn/blog_migrate/6b92f67d3be511b6e8c37561ce038363.jpeg)
所以,给你一个无序的数组,我们需要把这个数组构建成二叉堆,然后在通过堆顶逐个删除的方式来实现堆排序。
其实,也不算是删除了,相当于是把堆顶的元素与堆尾部在交换位置,然后在通过下沉的方式,把二叉树恢复成二叉堆。
代码如下:
public class HeapSort { /** * 下沉操作,执行删除操作相当于把最后 * * 一个元素赋给根元素之后,然后对根元素执行下沉操作 * @param arr * @param parent 要下沉元素的下标 * @param length 数组长度 */ public static int[] downAdjust(int[] arr, int parent, int length) { //临时保证要下沉的元素 int temp = arr[parent]; //定位左孩子节点位置 int child = 2 * parent + 1; //开始下沉 while (child < length) { //如果右孩子节点比左孩子小,则定位到右孩子 if (child + 1 < length && arr[child] > arr[child + 1]) { child++; } //如果父节点比孩子节点小或等于,则下沉结束 if (temp <= arr[child]) break; //单向赋值 arr[parent] = arr[child]; parent = child; child = 2 * parent + 1; } arr[parent] = temp; return arr; } //堆排序 public static int[] heapSort(int[] arr, int length) { //构建二叉堆 for (int i = (length - 2) / 2; i >= 0; i--) { arr = downAdjust(arr, i, length); } //进行堆排序 for (int i = length - 1; i >= 1; i--) { //把堆顶的元素与最后一个元素交换 int temp = arr[i]; arr[i] = arr[0]; arr[0] = temp; //下沉调整 arr = downAdjust(arr, 0, i); } return arr; } //测试 public static void main(String[] args) { int[] arr = new int[]{1, 3, 5,2, 0,10,6}; System.out.println(Arrays.toString(arr)); arr = heapSort(arr, arr.length); System.out.println(Arrays.toString(arr)); }}
对于堆的时间复杂度,我就直接给出了,有兴趣的可以自己推理下,还是不难的。堆的时间复杂度是 O (nlogn)。空间复杂度是 O(1)。
这里可能大家会问,堆排序的时间复杂度是O (nlogn),像快速排序,归并排序的时间复杂度也是 O(nlogn),那我在使用的时候该如何选择呢?
这里说明一下:快速排序是平均复杂度 O(logn),实际上,快速排序的最坏时间复杂度是O(n^2。),而像归并排序,堆排序,都稳定在O(nlogn)
我给出一个问题,例如给你一个拥有n个元素的无序数组,要你找出第 k 个大的数,那么你会选择哪种排序呢?
显然在这个问题中,选用堆排序是最好的,我们不用把数组全部排序,只需要排序到前k个数就可以了。至于代码如何实现,这个我就不给代码了,大家可以动手敲一敲。
完
每日推送原创文章,专注于写数据结构与算法,辅写计算机网络、计算机基础、Java,期待各位的关注,保证让你有所收获。