• 数据结构(十一) -- 树(三) -- 堆排序


    1. 堆排序基本介绍

    1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。

    2. 堆是具有以下性质的完全二叉树:

      1. 每个节点的值都大于或等于起左右孩子节点的值,成为大顶堆。注意:没有要求节点的左孩子和右孩子的值的大小关系
      2. 每个节点的值都小于或等于其左右孩子节点的值,成为小顶堆
    3. 大顶堆举例说明:
      在这里插入图片描述

    4. 小顶堆举例说明:
      在这里插入图片描述

    5. 一般升序使用大顶堆,降序采用小顶堆

    2. 堆排序的基本思想:

    1. 将待排序序列构造成一个大顶堆(大顶堆是构建成一个数组,并没有构建出一个真正的树,把树是以数组的形式存放的,然后将数组以大顶堆和小顶堆的遍历方式来进行遍历)
    2. 此时,整个序列的最大值就是堆顶的根节点
    3. 将其与末尾元素进行交换,此时末尾就是最大值
    4. 然后将剩余n-1个元素重新构造成一个堆,这样就会得到n个元素的次小值,如此反复,直到得到一个有序队列。

    3. 堆排序步骤图解:

    要求:
    给定一个数组:{4,6,8,5,9},要求使用堆排序方式,将数组升序排序。

    3.1 步骤一:构建初始堆,将给定无序序列构造成一个大顶堆

    1. 假设给定的无序序列结构如下:
      在这里插入图片描述

    2. 此时我们从最后一个非叶子节点开始(叶节点自然不用调整,第一个非叶子节点arr.length/2-1=5/2-1=1,也就是下面的6节点),从左到右,从上到下进行调整。
      在这里插入图片描述

    3. 找到第二个非叶子节点,由于4,9,8中9元素最大,4和9交换
      在这里插入图片描述

    4. 这时,交换导致了子根4,5,6结构混乱,继续调整,4,5,6中6最大,交换4和6
      在这里插入图片描述

    5. 此时,我们就将一个无序序列结构构造成了一个大顶堆

    3.2 步骤二:将堆顶元素和末尾元素进行交换,使末尾元素最大,然后继续调整堆,再将堆顶元素和末尾元素交换,得到第二大元素,以此直到完成。

    1. 将堆顶元素和末尾元素4进行交换:
      在这里插入图片描述

    2. 重新调整结构,使其继续满足堆定义
      在这里插入图片描述

    3. 再将堆顶元素8和末尾元素5进行交换,得到第二大元素8
      在这里插入图片描述

    4. 后序过程,继续调整交换,直到有序。
      在这里插入图片描述

    4. 总结堆排序基本思路:

    1. 将无序序列构建成一个堆,根据升序降序要求选择大顶堆还是小顶堆
    2. 将堆顶元素和末尾元素交换,将最大元素沉到数组末端
    3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素和当前末尾元素,反复执行,直到使整个序列有序。

    5. 代码实现:

    public class HeapSort {
    	public static void main(String[] args) {
    		//要求将数组进行升序排序
    		//int arr[] = {3, 7, 8, 10, 5, 13,2,7,4,15,9};
    		// 创建要给80000个的随机的数组
    		int[] arr = new int[8000000];
    		for (int i = 0; i < 8000000; i++) {
    			arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
    		}
    
    		System.out.println("排序前");
    		Date data1 = new Date();
    		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    		String date1Str = simpleDateFormat.format(data1);
    		System.out.println("排序前的时间是=" + date1Str);
    
    		heapSort(arr);
    
    		Date data2 = new Date();
    		String date2Str = simpleDateFormat.format(data2);
    		System.out.println("排序前的时间是=" + date2Str);
    		//System.out.println("排序后=" + Arrays.toString(arr));
    	}
    
    	//编写一个堆排序的方法
    	public static void heapSort(int arr[]) {
    		int temp = 0;
    		System.out.println("堆排序!!");
    
    //		//分步完成
    		//第一次调整
    //		adjustHeap(arr, 1, arr.length);
    //		System.out.println("第一次" + Arrays.toString(arr)); // 4, 9, 8, 5, 6
    //
    		// 第二次调整
    //		adjustHeap(arr, 0, arr.length);
    //		System.out.println("第2次" + Arrays.toString(arr)); // 9,6,8,5,4
    
    		//完成我们最终代码
    		//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
    		for(int i = arr.length / 2 -1; i >=0; i--) {
    			adjustHeap(arr, i, arr.length);
    		}
    		
    		/*
    		 * 2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
      			3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
    		 */
    		for(int j = arr.length-1;j >0; j--) {
    			//交换
    			temp = arr[j];
    			arr[j] = arr[0];
    			arr[0] = temp;
    			adjustHeap(arr, 0, j);
    		}
    
    		//System.out.println("数组=" + Arrays.toString(arr)); 
    	}
    
    	//将一个数组(二叉树), 调整成一个大顶堆
    	/**
    	 * 功能: 完成 将 以 i 对应的非叶子结点的树调整成大顶堆
    	 * 举例  int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
    	 * 如果我们再次调用  adjustHeap 传入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5, 4}
    	 * @param arr 待调整的数组
    	 * @param i 表示非叶子结点在数组中索引
    	 * @param lenght 表示对多少个元素继续调整, length 是在逐渐的减少
    	 */
    	public  static void adjustHeap(int arr[], int i, int lenght) {
    
    		int temp = arr[i];//先取出当前元素的值,保存在临时变量
    		//开始调整
    		//说明
    		//1. k = i * 2 + 1 k 是 i结点的左子结点
    		for(int k = i * 2 + 1; k < lenght; k = k * 2 + 1) {
    			if(k+1 < lenght && arr[k] < arr[k+1]) { //说明左子结点的值小于右子结点的值
    				k++; // k 指向右子结点
    			}
    			if(arr[k] > temp) { //如果子结点大于父结点
    				arr[i] = arr[k]; //把较大的值赋给当前结点
    				i = k; //!!! i 指向 k,继续循环比较
    			} else {
    				break;//!
    			}
    		}
    		//当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)
    		arr[i] = temp;//将temp值放到调整后的位置
    		// System.out.println(Arrays.toString(arr));
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    6. 自己测试,及过程详解

    以数列{3,7,8,10,5,13,2,7,4,15,9}为例:

    1. 第一个for循环做的事情:
    for(int i = arr.length / 2 -1; i >=0; i--) {
    	adjustHeap(arr, i, arr.length);
    }
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    1. 进入第二个for循环,将当前数组第一个元素,放到数组的最后,并重新进行大顶堆的调整。此时因为第一轮的时候,大顶堆已经基本构造完成(即保证了父节点一定是大于子节点的,也就是此时不考虑第一层的节点,第二层的两个节点一定是第二和第三大的数据),所以只需要对i=0即前三个数据进行处理。
    for(int j = arr.length-1;j >0; j--) {
    	//交换
    	temp = arr[j];
    	arr[j] = arr[0];
    	arr[0] = temp;
    	adjustHeap(arr, 0, j);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    1. 循环处理,略
    2. 输出的结果:从结果也映照了我们之前画的树的结果:
    堆排序!!
    [3, 7, 8, 10, 15, 13, 2, 7, 4, 5, 9]
    [3, 7, 8, 10, 15, 13, 2, 7, 4, 5, 9]
    [3, 7, 13, 10, 15, 8, 2, 7, 4, 5, 9]
    [3, 15, 13, 10, 9, 8, 2, 7, 4, 5, 7]
    [15, 10, 13, 7, 9, 8, 2, 3, 4, 5, 7]
    [13, 10, 8, 7, 9, 7, 2, 3, 4, 5, 15]
    [10, 9, 8, 7, 5, 7, 2, 3, 4, 13, 15]
    [9, 7, 8, 4, 5, 7, 2, 3, 10, 13, 15]
    [8, 7, 7, 4, 5, 3, 2, 9, 10, 13, 15]
    [7, 5, 7, 4, 2, 3, 8, 9, 10, 13, 15]
    [7, 5, 3, 4, 2, 7, 8, 9, 10, 13, 15]
    [5, 4, 3, 2, 7, 7, 8, 9, 10, 13, 15]
    [4, 2, 3, 5, 7, 7, 8, 9, 10, 13, 15]
    [3, 2, 4, 5, 7, 7, 8, 9, 10, 13, 15]
    [2, 3, 4, 5, 7, 7, 8, 9, 10, 13, 15]
    排序后=[2, 3, 4, 5, 7, 7, 8, 9, 10, 13, 15]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    7. 性能分析

    1. 在构建最大堆的时候,最多需要2N次比较和交换
    2. 堆排序最多需要2NlgN次比较和交换操作

    7.1 优点:

    堆排序最显著的优点是,他是就地排序,并且其最坏情况下时间复杂度为NlogN。经典的合并排序不是就地排序,它需要线性长度的额外空间,而快速排序其最坏时间复杂度为N^2

    在这里插入图片描述

    7.2 缺点:

    堆排序对时间和空间都进行了优化,但是:

    1. 其内部循环要比快速排序要长。
    2. 并且其操作在N和N/2之间进行比较和交换,当数组长度比较大的时候,对CPU缓存利用效率比较低。
    3. 非稳定性排序
  • 相关阅读:
    [护网杯 2018]easy_tornado 1(两种解法!)
    【状语从句练习题】since 的时态问题
    力扣刷题 day56:10-26
    虚拟机服务器中了lockbit3.0勒索病毒怎么办,lockbit3.0勒索病毒解密数据恢复
    MIT 6.NULL The Missing Semester of Your CS Education(2)
    多线程06:条件变量
    双链笔记软件 Roam Edit 的优点、缺点、评价及学习资源
    系统架构设计师【第12章】: 信息系统架构设计理论与实践 (核心总结)
    MySQL-DML语言-数据库操作语言-
    接口与抽象类的区别
  • 原文地址:https://blog.csdn.net/weixin_39724194/article/details/126924166