数据结构017_树的应用(堆排序)
一、堆排序基本介绍
1)堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序也是一种选择排序,它的最坏、最好、平均时间复杂度都是O(nlogn),不稳定排序。
2)堆是完全二叉树(完全二叉树概念可以参考上一篇,我自己都忘记,又回头看)。大顶堆:每个结点的值都大于或者等于其左右孩子结点的值。小顶堆:每个结点的值都小于或者等于其左右孩子结点的值。注意,这里没有要求结点左孩子和右孩子的值的大小关系。
3)它使用顺序存储,arr作为存储堆的数组,大顶堆特点:arr[i]>=arr[2*i+1] && arr[i]>=arr[2*i+2]。(i对应第几个结点,从0开始编号)小顶堆也是这么个意思,我不写啦。
4)一般升序采用大顶堆,降序采用小顶堆。【为什么?】【这里我搜到的原因是如果使用小顶堆进行升序排序,当取出最小的堆顶元素,小顶堆的性质就变了,找不到第二小的元素了,还要重新建堆。这里我很疑惑,使用大顶堆取出最大的堆顶元素,也依旧要重新调整堆啊,这里还是不懂,等下面写完代码看看有什么新的体会吧,自己试试用小顶堆排升序最好】
二、堆排序基本思想
以大顶堆排升序为例:
1)将排序序列构造成大顶堆
2)此时整个序列的最大值就是堆顶的根节点
3)将其与末尾元素进行交换,此时末尾就是最大值
4)然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次大值。如此反复执行,就能得到一个有序序列。
三、图例:
步骤一.构造大顶堆(升序一般采用大顶堆,降序小顶堆)
1)假定给定无序序列结构如下:
2)此时我们从最后一个非叶子结点开始(叶子结点不用调整,第一个非叶子节点arr.length/2-1=5/2-1=1,也就是结点6)【这又是为什么,公式来源】,从左到右,从下至上进行调整。6、5、9结点调整成大顶堆。
3)找到第二个非叶子节点4,将4、9、8结点调整成大顶堆结构,4和9交换。
4)这时,交换导致子根[4,5,6]结构混乱,继续调整,交换4和6。
5)这时,我们就将一个无序序列构造成一个大顶堆。
步骤二.将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整,再将堆顶元素与末尾元素交换。得到第二大元素。如此反复进行交换、重建、交换。
1)将堆顶元素9和末尾元素4交换
2)重新调整结构,使其继续满足堆定义
3)再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
4)后续过程继续进行调整,交换,反复进行,最终整个序列有序
四、代码实现
package com.njcx.tree; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import javax.xml.crypto.Data; public class HeapSort { // 要求将数组进行升序排序 public static void main(String[] args) { // int[] arr = { 4, 6, 8, 5, 9 }; // 堆排序的速度非常快,时间复杂度O(nlogn),测试八百万个数据的排序时间,才两秒,很厉害 // 创建要给80000个随机的数组 int[] arr = new int[8000000]; for (int i = 0; i < 8000000; i++) { arr[i] = (int) (Math.random() * 8000000); } System.out.println("排序前"); Date date1 = new Date(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss"); String dateStr = simpleDateFormat.format(date1); System.out.println("排序前的时间是:" + dateStr); heapSort(arr); Date date2 = new Date(); String date2Str = simpleDateFormat.format(date2); System.out.println("排序后的时间是:" + date2Str); System.out.println(""); // System.out.println("排序后的数组"+Arrays.toString(arr)); // //小数据量测试一下对不对再进行时间测试 } /** * 堆排序的方法 * * @param arr */ public static void heapSort(int[] arr) { System.out.println("堆排序"); int temp = 0; // 1.创造大顶堆 for (int i = arr.length / 2 - 1; i >= 0; i--) { adjustHeap(arr, i, arr.length); } // 2.将大顶堆堆顶元素放在数组最末尾,对其余元素继续调整大顶堆,继续放 for (int j = arr.length - 1; j > 0; j--) { temp = arr[j]; arr[j] = arr[0]; arr[0] = temp; adjustHeap(arr, 0, j); // 这里我不是很能理解为什么形参i传的是0 } // System.out.println("堆排序的数组是" + Arrays.toString(arr));//测试时间的时候就不要打印了 } /** * 将一个数组(二叉树)调整成一个大顶堆 举例:arr={4,6,8,5,9} → i=1 ->adjustHeap → * arr={4,9,8,5,6}; 如果我们再次调用adjustHeap,传入的是i=0 → {4,9,8,5,6} → {9,6,8,5,4} * * @param arr * 待调整的数组 * @param i * 表示非叶子结点在数组中的索引 * @param length * 表示有多少个元素继续调整,length是在主键减小 */ public static void adjustHeap(int[] arr, int i, int length) { int temp = arr[i];// 先取出当前元素的值保存临时变量 // 开始调整 // 1.k代表的是以i为非叶子结点的左子结点 for (int k = i * 2 + 1; k < length; k = k * 2 + 1) { if (k + 1 < length && arr[k] < arr[k + 1]) // 说明i结点的左子结点的值小于右子结点的值、 k++;// k指向右子结点 if (arr[k] > temp) { // 如果子结点大于父节点, arr[i] = arr[k]; // 把较大的值赋给当前结点 i = k; // 让i指向k继续循环比较【!!!非常重要的一步】 } else { break; // 【这里特别不好理解,因为我们调整的时候是从左到右从上到下,i下的都调整过了】 } } // 当for循环结束后,我们已经将以i为父节点的树的最大值放在了i原先的位置上。这里是局部大顶堆 arr[i] = temp;// 将temp值放在调整后的位置 } }