堆排序
https://blog.csdn.net/qq_28063811/article/details/93034625
1、首先了解堆是什么
堆是一种数据结构,一种叫做完全二叉树的数据结构。
2、堆的性质
这里我们用到两种堆,其实也算是一种。
大顶堆:每个节点的值都大于或者等于它的左右子节点的值。
小顶堆:每个节点的值都小于或者等于它的左右子节点的值。
小顶堆图片是错误的.
如上所示,就是两种堆。
如果我们把这种逻辑结构映射到数组中,就是下边这样
这个数组 arr 逻辑上就是一个堆。
从这里我们可以得出以下性质(重点)
对于大顶堆:arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]
对于小顶堆:arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]
3、堆排序的基本思想
了解了以上内容,我们可以开始探究堆排序的基本思想了。
堆排序的基本思想是:1、将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;2、将堆顶元素和最后一个元素交换,然后将剩下的节点重新构造成一个大顶堆;3、重复步骤 2,如此反复,从第一次构建大顶堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大顶堆的尾部。最后,就得到一个有序的序列了。
假设给定的无序序列 arr 是:
1、将无序序列构建成一个大顶堆。
首先我们将现在的无序序列看成一个堆结构,一个没有规则的二叉树,将序列里的值按照从上往下,从左到右依次填充到二叉树中。
根据大顶堆的性质,每个节点的值都大于或者等于它的左右子节点的值。所以我们需要找到所有包含子节点的节点,也就是非叶子节点,然后调整他们的父子关系,非叶子节点遍历的顺序应该是从下往上,这比从上往下的顺序遍历次数少很多,因为,大顶堆的性质要求父节点的值要大于或者等于子节点的值,如果从上往下遍历,当某个节点即是父节点又是子节点并且它的子节点仍然有子节点的时候,因为子节点还没有遍历到,所以子节点不符合大顶堆性质,当子节点调整后,必然会影响其父节点需要二次调整。但是从下往上的方式不需要考虑父节点,因为当前节点调整完之后,当前节点必然比它的所有子节点都大,所以,只会影响到子节点二次调整。相比之下,从下往上的遍历方式比从上往下的方式少了父节点的二次调整。
那么,该如何知道最后一个非叶子节点的位置,也就是索引值?
对于一个完全二叉树,在填满的情况下(非叶子节点都有两个子节点),每一层的元素个数是上一层的二倍,根节点数量是 1,所以最后一层的节点数量,一定是之前所有层节点总数 + 1,所以,我们能找到最后一层的第一个节点的索引,即节点总数 / 2(根节点索引为 0),这也就是第一个叶子节点,所以第一个非叶子节点的索引就是第一个叶子结点的索引 - 1。那么对于填不满的二叉树呢?这个计算方式仍然适用,当我们从上往下,从左往右填充二叉树的过程中,第一个叶子节点,一定是序列长度 / 2,所以第一个非叶子节点的索引就是 arr.length / 2 -1。
现在找到了最后一个非叶子节点,即元素值为 2 的节点,比较它的左右节点的值,是否比他大,如果大就换位置。这里因为 1<2,所以,不需要任何操作,继续比较下一个,即元素值为 8 的节点,它的左节点值为 9 比它本身大,所以需要交换
交换后的序列为:
因为元素 8 没有子节点,所以继续比较下一个非叶子节点,元素值为 5 的节点,它的两个子节点值都比本身小,不需要调整;然后是元素值为 4 的节点,也就是根节点,因为 9>4,所以需要调整位置
交换后的序列为:
9 | 5 | 4 | 2 | 3 | 8 | 7 | 1 |
此时,原来元素值为 9 的节点值变成 4 了,而且它本身有两个子节点,所以,这时需要再次调整该节点
交换后的序列为:
到此,大顶堆就构建完毕了。满足大顶堆的性质。
2、排序序列,将堆顶的元素值和尾部的元素交换
交换后的序列为:
然后将剩余的元素重新构建大顶堆,其实就是调整根节点以及其调整后影响的子节点,因为其他节点之前已经满足大顶堆性质。
交换后的序列为:
然后,继续交换,堆顶节点元素值为 8 与当前尾部节点元素值为 1 的进行交换
交换后的序列为:
重新构建大顶堆
交换后的序列为:
继续交换
交换后的序列为:
4、堆排序的代码实现(java 版本)
public class HeapSort { public static void heapSort(int[] arr) { if (arr == null || arr.length == 0) { return; } int len = arr.length; // 构建大顶堆,这里其实就是把待排序序列,变成一个大顶堆结构的数组 buildMaxHeap(arr, len); // 交换堆顶和当前末尾的节点,重置大顶堆 for (int i = len - 1; i > 0; i--) { swap(arr, 0, i); len--; heapify(arr, 0, len); } } private static void buildMaxHeap(int[] arr, int len) { // 从最后一个非叶节点开始向前遍历,调整节点性质,使之成为大顶堆 for (int i = (int)Math.floor(len / 2) - 1; i >= 0; i--) { heapify(arr, i, len); } } private static void heapify(int[] arr, int i, int len) { // 先根据堆性质,找出它左右节点的索引 int left = 2 * i + 1; int right = 2 * i + 2; // 默认当前节点(父节点)是最大值。 int largestIndex = i; if (left < len && arr[left] > arr[largestIndex]) { // 如果有左节点,并且左节点的值更大,更新最大值的索引 largestIndex = left; } if (right < len && arr[right] > arr[largestIndex]) { // 如果有右节点,并且右节点的值更大,更新最大值的索引 largestIndex = right; } if (largestIndex != i) { // 如果最大值不是当前非叶子节点的值,那么就把当前节点和最大值的子节点值互换 swap(arr, i, largestIndex); // 因为互换之后,子节点的值变了,如果该子节点也有自己的子节点,仍需要再次调整。 heapify(arr, largestIndex, len); } } private static void swap (int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
5、复杂度分析
因为堆排序无关乎初始序列是否已经排序已经排序的状态,始终有两部分过程,构建初始的大顶堆的过程时间复杂度为 O(n),交换及重建大顶堆的过程中,需要交换 n-1 次,重建大顶堆的过程根据完全二叉树的性质,[log2(n-1),log2(n-2)...1] 逐步递减,近似为 nlogn。所以它最好和最坏的情况时间复杂度都是 O(nlogn),空间复杂度 O(1)。