赞
踩
1)堆结构就是用数组实现的完全二叉树结构。(完全二叉树:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。)
2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
4)堆结构的heapInsert(插入)与heapify(调整)操作
5)堆结构的增大和减少
6)优先级队列结构,就是堆结构
取决于我们有没有动态改信息的需求。
语言提供的堆结构,如果你动态改数据,不保证依然有序
[ 手写堆 ],因为增加了对象的位置表,所以能够满足动态改信息的需求
在正式进入堆的排序中,我们先对齐一下需要的信息。
完全二叉树父节点与子节点编号的关系:
二叉树中父节点为k,它的左子节点下标为2k+1,右子节点是2k+2。
如下图解释:
论证过程用等比数列通项公式可证,略。
先让整个数组都变成大根堆结构,建立堆的过程:
a: 从上到下的方法,时间复杂度为O(N * logN)
b: 从下到上的方法,时间复杂度为O(N)
从顶输出:把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,这一步时间复杂度为O(N * logN)
堆的大小减小成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;
}
}
建立好之后,每次把堆顶的数弹出的数,就是排好序的。
那么弹出之后,依然需要调整。这个过程是什么样呢:
我们先把弹出的数和最后一个数交换。用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; } }
全部排序过程即:
// 堆排序额外空间复杂度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) } }
问: 为什么在建堆的过程从上到下还是从下建堆 ,会影响时间复杂度呢?
这是个好问题。我们来分析下:
再比较一下 刚才的两个过程。如图:
从上到下建堆 是先从【最上边】的位置开始判断是不是大跟堆,依次扩大数的范围。【新加入的数还能不能再向上(跟父亲比)】确定每次加入数之后,前边的数据依然保持最大堆。所以需要每次加入的数去和其父亲比较,如果比父亲大,进行交换。继续和交换后所在位置的父亲比较。
而从下到上建堆。是先确定【最下边】的是不是大跟堆。【新加入的数还能不能再向下(跟大儿子比)】
从上图笔记,计算出从下到上 时间复杂度是 o(n)
和从上到下的实质区别是 ,大量的节点是比较的层数变少了。
做完笔记逐步有感觉了。这周虽然刷了5道题,但是已经往后看了2集视频了,对应的题还没来得及刷。比之前的节奏是快了的,往后会慢慢体现出来的。
第四周进度: 应刷 14 道,实刷 5道
用时 8h+
[ 比较器 ] 打卡
2022-05-02
[ 堆排序 从下到上] 打卡
2022-05-07
[ 堆排序 从上到下] 打卡
2022-05-07
[ 手写堆 ] 打卡
2022-05-06
[ 相对几乎有序数组排序 ] 打卡
2022-05-07
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。