当前位置:   article > 正文

算法-堆排序及时间复杂度分析_堆排序的时间复杂度

堆排序的时间复杂度

堆结构

1)堆结构就是用数组实现的完全二叉树结构。(完全二叉树:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。)

2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
4)堆结构的heapInsert(插入)与heapify(调整)操作
5)堆结构的增大和减少
6)优先级队列结构,就是堆结构

语言提供的堆结构 vs 手写的堆结构

取决于我们有没有动态改信息的需求。

语言提供的堆结构,如果你动态改数据,不保证依然有序

[ 手写堆 ],因为增加了对象的位置表,所以能够满足动态改信息的需求

补充知识

在正式进入堆的排序中,我们先对齐一下需要的信息。

完全二叉树父节点与子节点编号的关系:
二叉树中父节点为k,它的左子节点下标为2k+1,右子节点是2k+2。

如下图解释:
在这里插入图片描述
论证过程用等比数列通项公式可证,略。

堆排序

  1. 先让整个数组都变成大根堆结构,建立堆的过程:
    a: 从上到下的方法,时间复杂度为O(N * logN)
    b: 从下到上的方法,时间复杂度为O(N)

  2. 从顶输出:把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,这一步时间复杂度为O(N * logN)

  3. 堆的大小减小成0之后,排序完成

即 整体时间复杂度 O(N * logN) 【这句话可以看完全文再过来看】

整体过程:

具体过程

从上到下建堆,排【10,8,7,19,45】
则从上开始,第0个位置开始建堆。执行insert 过程。即假设每次新增一个元素增加到最大堆中 。每次和其父亲比较。如果大于父亲就交换。知道无父或小于父的时候停止。

在这里插入图片描述

insert 过程代码:

	// arr[index]刚来的数,往上
	public static void heapInsert(int[] arr, int index) {
		while (arr[index] > arr[(index - 1) / 2]) {
			swap(arr, index, (index - 1) / 2);
			index = (index - 1) / 2;
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

建立好之后,每次把堆顶的数弹出的数,就是排好序的。
那么弹出之后,依然需要调整。这个过程是什么样呢:
我们先把弹出的数和最后一个数交换。用heapsize --,认定最后一个数已经弹出了。然后调整当前从最后一个位置换到堆顶的元素,进行heapify ,即 依次和下边的最大儿子比较交换即可。
在这里插入图片描述

heapify 过程:


	// arr[index]位置的数,能否往下移动
	public static void heapify(int[] arr, int index, int heapSize) {
		int left = index * 2 + 1; // 左孩子的下标
		while (left < heapSize) { // 下方还有孩子的时候
			// 两个孩子中,谁的值大,把下标给largest
			// 1)只有左孩子,left -> largest
			// 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
			// 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> largest
			int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
			// 父和较大的孩子之间,谁的值大,把下标给largest
			largest = arr[largest] > arr[index] ? largest : index;
			if (largest == index) {
				break;
			}
			swap(arr, largest, index);
			index = largest;
			left = index * 2 + 1;
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

全部排序过程即:

// 堆排序额外空间复杂度O(1)
	public static void heapSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// 从上到下建堆 O(N*logN)
//		for (int i = 0; i < arr.length; i++) { // O(N)
//			heapInsert(arr, i); // O(logN)
//		}
		// 从下到上建堆  O(N)
		for (int i = arr.length - 1; i >= 0; i--) {
			heapify(arr, i, arr.length);
		}
		int heapSize = arr.length;
		swap(arr, 0, --heapSize);
		// O(N*logN)
		while (heapSize > 0) { // O(N)
			heapify(arr, 0, heapSize); // O(logN)
			swap(arr, 0, --heapSize); // O(1)
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

问: 为什么在建堆的过程从上到下还是从下建堆 ,会影响时间复杂度呢?

这是个好问题。我们来分析下:
再比较一下 刚才的两个过程。如图:
在这里插入图片描述

从上到下建堆 是先从【最上边】的位置开始判断是不是大跟堆,依次扩大数的范围。【新加入的数还能不能再向上(跟父亲比)】确定每次加入数之后,前边的数据依然保持最大堆。所以需要每次加入的数去和其父亲比较,如果比父亲大,进行交换。继续和交换后所在位置的父亲比较。

而从下到上建堆。是先确定【最下边】的是不是大跟堆。【新加入的数还能不能再向下(跟大儿子比)】

在这里插入图片描述

从上图笔记,计算出从下到上 时间复杂度是 o(n)
和从上到下的实质区别是 ,大量的节点是比较的层数变少了。

与堆有关的题目

  • [ 相对几乎有序数组排序 ]

本周打卡记录

做完笔记逐步有感觉了。这周虽然刷了5道题,但是已经往后看了2集视频了,对应的题还没来得及刷。比之前的节奏是快了的,往后会慢慢体现出来的。

 第四周进度: 应刷 14 道,实刷 5道
 用时 8h+
  • 1
  • 2

[ 比较器 ] 打卡 2022-05-02

[ 堆排序 从下到上] 打卡 2022-05-07

[ 堆排序 从上到下] 打卡 2022-05-07

[ 手写堆 ] 打卡 2022-05-06

[ 相对几乎有序数组排序 ] 打卡 2022-05-07

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/900715
推荐阅读
相关标签
  

闽ICP备14008679号