内容介绍
堆排序简介
前面咱们已经介绍了堆结构,也可以构造一个堆结构。若是对堆结构不了解的同窗能够看一下以前的文章《动画学堆结构,一篇一看就懂的堆结构文章》。咱们稍微回顾一下,根节点最大的堆叫作最大堆
或大根堆
,以下图: java
获取最大堆的最大值,其实就是获取堆顶的元素。对于堆这种数据结构一般是将堆顶的元素和堆中最后一个元素换位置,最大值就到了最后一个位置,而后从堆中排除这个最大的元素,当最后一个元素交换到最前面时,此时就不知足堆的性质了,咱们须要将最前面这个元素经过ShiftDown
(下沉)的手段让堆继续知足堆的规则。算法
堆获取最大值能够分红两个步骤:编程
- 将堆中最前面的最大值和最后一个元素交换位置。
- 使用
ShiftDown
让最前面的元素下沉到合适的位置,依然知足堆的性质。 动画演示效果以下:
堆排序的思想
咱们知道堆顶的元素就是堆中的最大值,所以咱们每次从堆中取出并移除最大值(实际上是将堆顶的最大值移动到了堆的最后面),而后ShiftDown
使这个二叉树依然知足堆的规则,堆中第二大的数据就会到堆顶,再次取出堆顶的最大值,其实就是全部数据中的第二大值,依次类推,因而就完成了堆排序。api
堆排序动画演示
通常没有特殊要求排序算法都是升序排序,小的在前,大的在后。数组由{5, 3, 1, 9, 7, 2, 8, 6} 这8个无序元素组成。 数组
堆排序分析
如上图所示,当咱们将堆顶的最大值9移除后,堆中的第二大值8就会到堆顶来。当咱们再次将8移除后,堆中剩余元素的最大值会到堆顶来。微信
堆删除8后的效果: 数据结构
堆删除7后的效果: 性能
堆删除6后的效果: 优化
堆删除5后的效果: 动画
堆删除4后的效果:
堆删除3后的效果:
堆删除2后的效果:
堆删除1后的效果:
咱们能够看到堆排序算法的步骤:
- 把无序二叉树构建成二叉堆。
- 循环删除堆顶元素,移到数组尾部,调整二叉堆,获得新堆中的最大值放到堆顶。
堆排序代码编写
public class HeapSort { public static void main(String[] args) { int[] arr = {6, 3, 7, 5, 8, 2, 1, 4, 9}; heapSort(arr); System.out.println("堆排序后:" + Arrays.toString(arr)); } /** * 堆排序 * @param arr 待排序的数组 */ public static void heapSort(int[] arr) { heapify(arr); System.out.println("构建堆:" + Arrays.toString(arr)); // 让堆顶元素和堆最后一个元素交换,其实就是数组最前面数据,和后面的数据交换 for (int i = arr.length-1; i > 0; i--) { swap(arr, i, 0); shiftDown(arr, 0, i); } } /** * heapify将无序的彻底二叉树调整为二叉堆 * @param arr 待调整的数组 */ private static void heapify(int[] arr) { // 从非叶子节点开始,Shift Down将每一个子树构建成最大堆 for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) { shiftDown(arr, i, arr.length); } } /** * 下沉操做,将指定元素下沉到子树的合适位置,使这个颗树知足堆的规则。 * @param arr 待调整的数组 * @param index 要下沉的元素索引 * @param count 堆的有效范围 */ private static void shiftDown(int[] arr, int index, int count) { // 下沉操做时减小赋值,先保存这个要下沉的元素,后面找到合适位置直接交换 int temp = arr[index]; // j表示左孩子索引 int childIndex = 2*index + 1; // 循环找子孩子交换位置。左孩子不能越界 while (childIndex < count) { // 判断是否有有孩子,而且右孩子是否大于左孩子 if (childIndex+1 < count && arr[childIndex+1] > arr[childIndex]) { childIndex++; // 若是是,和右孩子交换 } // 若是当前节点大于两个孩子,就不须要交换 if (temp > arr[childIndex]) break; // 当前节点小于子孩子,将当前节点和较大的子孩子交换 // 无需真正交换,记录这个要交换的索引 arr[index] = arr[childIndex]; // 再判断下一层 index = childIndex; childIndex = 2*index + 1; } arr[index] = temp; } public static void swap(int[] arr, int start, int end) { if (start == end) return; int temp = arr[start]; arr[start] = arr[end]; arr[end] = temp; } }
堆排序的复杂度
堆排序的时间复杂度:堆排序的运行时间主要是消耗在开始构建堆和在取出最大值后重建堆时的数据下沉上。在构建堆的过程当中,是从彻底二叉树最后一个非叶子节点开始构建,最后一个非叶子节点为(n-1)/2
,将它与其孩子进行比较和互换,对于每一个非叶子节点最多会进行两次比较,因此构建堆的时间复杂度为0(n)。排序时,堆顶元素须要和堆中最后一个有效元素交换位置并下沉,时间复杂度是O(logi),而且有n-1次获取堆顶最大值的过程所以,在依次获取最大值时的时间复杂度是O(nlogn)。构建堆和获取堆顶最大值排序是两个先后操做,所以堆排序整体的时间复杂度是O(nlogn)。
咱们能够看到堆排序对待排序的数据不敏感,不管数据怎么样,堆排序的最好,最坏,平均时间复杂度都是O(nlogn),没有优化的空间,所以真正在排序的时候不会选择堆排序,而是选择优化性能更好的快速排序。
堆排序的空间复杂度:在堆排序的过程当中只须要一个额外的变量记录要交换的数据,所以堆排序的空间复杂度为O(1)。堆排序在获得一个最大值后,会让堆顶的元素下沉到合适的位置,所以堆排序是不稳定的排序算法。
总结
堆排序的过程:
- 将一颗彻底二叉树构建成堆
- 循环获取堆顶的最大值,放到堆的后面,并重建堆。
原创文章和动画制做真心不易,您的点赞就是最大的支持! 想了解更多文章请关注微信公众号:表哥动画学编程