数据结构和算法 - 堆和堆排序
一、堆结构的定义
堆通常是一个可以被看做一棵 完全二叉树的 数组对象,堆是非线性数据结构。堆总是满足下列性质:
- 堆中的某个节点的值总是不大于或者不小于其父节点的值
- 堆总是一个完全二叉树
倘若子树中 子节点中的值都小于父节点 那么这个堆称为小根堆,反之称为大根堆
二、堆的实现、
2.1 堆的向上调整算法
2.1.1 思路
数组中新添加进来的元素,与自己的父元素比较 若比自己的父元素大则交换,一直比到索引为 0 或者直到此元素没有自己的父元素大为止
堆的向下调整算法
2.2.1 思路
数组是一个堆,从某个指定的位置开始和自己的左右两个叶节点比较 如果这个位置比某个叶子节点小就交换,直到不比叶子节点小为止,或者已经到了堆的最后位置。
2.3 堆结构完整代码
三、堆排序
3.1 思路
- heapSort 方法:
- 首先,如果数组为空或者只有一个元素,那么无需排序,直接返回。
- 然后,使用 heapify 方法从最后一个非叶子节点开始,向前遍历数组,将每个子树调整为大根堆。这一步的目的是将数组初始化为大根堆。
- 在数组被初始化为大根堆之后,开始排序过程。每次将堆顶元素(最大元素)与当前堆的最后一个元素交换,然后将堆的大小减1,并重新调整堆。重复这个过程直到堆的大小为0,此时数组已经排序完成。
- heapify 方法:
- 该方法用于调整以 i 为根节点的子树为最大堆。
- 首先,计算左子节点的索引 left。
- 然后,进入一个循环,在该循环中,找到 i、左子节点和右子节点中的最大值。
- 如果最大值不是 i,那么将最大值与 i 交换,并更新 i 和 left 的值,继续循环。
- 如果最大值就是 i,那么退出循环,此时以 i 为根节点的子树已经是一个最大堆。
- heapInsert 方法:
- 这个方法用于在构建堆的过程中,将新插入的元素插入到正确的位置,以保持堆的性质。但是在这段代码中,heapInsert 方法并没有在 heapSort 方法中被调用,因此这段代码中的 heapInsert 是多余的。也可以使用heapInsert 方法初始化堆结构。
- swap 方法:
- 这个方法用于交换数组中的两个元素。
3.2 代码
四、堆排序的时间复杂度
堆排序的时间复杂度是 (N*logN)
倘若 是使用 heapInsert 来插入元素,则是这个 for 循环是O(N*logN),这个操作是将数组转为大根堆
因为每次插入一个元素需要和自己的父元素作比较,每次比较是 logN级别的所以是(N*logN)
但是这个操作如果数组是一次性全部拿到 而不是一个一个给的,是可以收敛为O(N)的。
我们可以把这个数组直接想象成为一个完全二叉树,虽然它不是大根堆。例如:

我们想象这个数组就是完全二叉树,然后从下往上遍历,依次是 76 43 5 43 12 32 22 43,依次进行heapify 最后一层就变成了大根堆,然后 再遍历89 23 344 13 依然是heapify 这样倒数第二层以及最后一层都是大根堆,然后依次12 21、10这样下来数组就变成了大根堆的结构。这个操作的时间复杂度是O(N)。为什么呢?
假设数组有 N 个元素,那么叶节点差不多就有N/2个也就是最下面一层,刚刚上述数组有 15 个元素 叶节点有 8 个,即使不是完全满二叉树也是一样的,例如下图:有四个元素,叶节点有两个

这些节点在做heapify 时候,只需要判断自己,N/2 * 1,倒数第二层的节点 从数量级来说大概是有N/4个 在做heapify时候,最多需要判断三个单位往下沉的的次数和层数有关,所以是N/4 * 2
那么第三层有N/8个节点,最多需要判断 加交换N/8 * 6次
整个时间复杂度 T(N) = N/2 * 1 + N/4 * 2 + N/8 * 4 + N/16 * 6 +...... 相乘后忽略常数项的时间复杂度是 O(N)这个是里面所有操作的时间复杂度。
五、面试题
5.1 题目一
已知一个几乎有序的数组。几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的。请选择一个合适的排序策略,对这个数组进行排序。
举例:
原始数组 arr = [3,4,1,2,5] k = 2
排序后数组 arr = [1,2,3,4,5]
1 排序后从 索引为 2 的位置到 索引为 0 的位置 移动了两个位置
2 排序后从 索引为 3 位置移动到 索引为 1 的位置 移动了两个位置
3 排序后从 索引为0 位置移动到 索引 2 的位置 移动了两个位置
4 排序后从 索引为 1 移动到索引 索引 3 的位置 移动了两个位置
5 排序后从索引为 4 的位置移动索引为 4 的位置 移动了零个位置
5.1.1 思路
可以使用小根堆,先将数组的前 k 个值放入 小根堆中 每次弹出的就是最小值 然后弹出一个值 再放入一个值,最后将小根堆中的值全部弹出 。时间复杂度为O(N*logK)k 比 N 小 就是最优解
5.1.2 代码
题目二
合并k个已排序的链表
描述
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。
数据范围:节点总数 0≤n≤5000,每个节点的val满足 ∣val∣<=1000
要求:时间复杂度 O(nlogn)
示例1
输入:[{1,2,3},{4,5,6,7}]
返回值:{1,2,3,4,5,6,7}
5.2.1 思路
将每个链表的头节点压入小根堆,然后弹出的第一个元素是需要返回的元素,然后判断弹出元素是否有下个元素,如果有 再压入小根堆 然后直到小根堆中的值全部弹出。
5.2.2 代码
题目三
牛客链接测试:线段重合_牛客题霸_牛客网
给定很多线段,每个线段都有两个数[start, end],表示线段开始位置和结束位置,左右都是闭区间
规定:
- 线段的开始和结束位置一定都是整数值
- 线段重合区域的长度必须>0
返回线段最多重合区域中,包含了几条线段
思路一
使用一个比较笨的办法,我们先将数组排序 求出一个最大值 max 和一个最小值 min,这样就能确定数据是从 min开始 max结束。那么我们求 min + 0.5也就是求 min 到 max 之间每个数加 0.5 有多少个线段在这个数中,最后求出最大值即可。时间复杂度为 O((max-min)*N)

代码
思路二
使用堆排序,我们可以将数组的开始位置进行排序,然后依次枚举数组的开始位置 具体操作如下
- 建立一个小根堆,用于存放线段的结束位置
- 依次枚举线段,将小根堆中的数小于线段开始位置的数弹出,然后压入线段的结束,计算小根堆中的数的数量
- 依次比较求最大值
时间复杂度为 O(N*logN)
代码
完整代码以及对数器
牛客提交代码
题目四
leetCode2208
https://leetcode.cn/problems/minimum-operations-to-halve-array-sum/description/
5.4.1 实现思路
题目要求是求出数组总和 sum,然后每次取一个数的一半 累计和 ansSum要求是 求 ansSum需要加多少次才能到达 sum的一半。如果说每次都取数组中的最大值,然后取一半的值加起来 求出次数就是答案
使用大根堆,每次弹出最大值,然后取一半 再放进去 直到达到 ansSum>=sum 位置一共加了多少次即可
