堆结构与堆排序
堆结构
逻辑结构:完全二叉树的形式(从左到右,一层层覆盖,且每个父节点最多有两个子节点)
物理结构:采用数组进行存储
如何保证完全二叉树与数组前缀范围的映射?
- 下标为i的元素,父亲结点为(i-1)/2,左孩子为2i+1,右孩子为2i+2(结点0的父亲结点仍然是自己,根节点)
大小根堆的区别:
- 大根堆父节点大于所有子节点,小根堆父节点小于所有子节点
堆的调整:
- 向上调整 heapInsert:从当前i结点开始调整,如果当前节点大于他的父节点,则交换当前结点与父节点元素,并让i等于父节点下标,i= (i-1)/2,直到不再大于其父亲结点或者到达下标为0结点为止(0的父节点也是0,显然不会大于)
// i位置的数,向上调整大根堆
// arr[i] = x,x是新来的!往上看,直到不比父亲大,或者来到0位置(顶)
public static void heapInsert(int[] arr, int i) {
while (arr[i] > arr[(i - 1) / 2]) {
swap(arr, i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
- 向下调整heapify:
- 第一步让i等于其左孩子
- 只要i小于size就不断迭代循环
- 若右孩子结点存在并且左孩子小于右孩子,则最大索引best等于右孩子索引,否则左孩子索引
- 若best元素大于i元素交换best与i元素
- 若未交换则直接break,说明不用再向下调整了,子树都比你小
- 让i等于best
- 让i等于其左孩子继续迭代
// i位置的数,变小了,又想维持大根堆结构
// 向下调整大根堆
// 当前堆的大小为size
public static void heapify(int[] arr, int i, int size) {
int l = i * 2 + 1;
while (l < size) {
// 有左孩子,l
// 右孩子,l+1
// 评选,最强的孩子,是哪个下标的孩子
int best = l + 1 < size && arr[l + 1] > arr[l] ? l + 1 : l;
// 上面已经评选了最强的孩子,接下来,当前的数和最强的孩子之前,最强下标是谁
best = arr[best] > arr[i] ? best : i;
if (best == i) {
break;
}
swap(arr, best, i);
i = best;
l = i * 2 + 1;
}
}
- 向上调整与向下调整时间复杂度分析:
树层高为logn,向上调整与向下调整的时间复杂度均为logN。
堆排序
- 代码
从顶到底建堆,heapInsert方法,每次插入一个结点,开始向上调整直到他的位置,可以分析出复杂度为时间复杂度O(n(ogn),log1+|og2+log3+.+logn->O(n(ogn)
或者使用倍增分析法分析时间复杂度:一个复杂度既是N个数据量为时为下线,2N个数据量时为上限,那么他的复杂度就是这个复杂度,因为常量不会影响复杂度**
这里可以进行如下分析:
如果数据量为N,log1+|og2+log3+.+logn其上限为nlogn,如果数据量为2n,其时间复杂度的下限为nlogn,但数据量不会影响时间复杂度,故可以得出时间复杂度为nlogn
// 从顶到底建立大根堆,O(n * logn)
// 依次弹出堆内最大值并排好序,O(n * logn)
// 整体时间复杂度O(n * logn)
public static void heapSort1(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) {
heapInsert(arr, i);
}
int size = n;
while (size > 1) {
swap(arr, 0, --size);
heapify(arr, 0, size);
}
}
- 自底向上建堆
建堆过程优化到了logN,具体过程不赘述了,只需要直到大部分的结点调整层数少,少部分的结点调整层数多,这一点与自顶向上建堆相反
// 从底到顶建立大根堆,O(n)
// 依次弹出堆内最大值并排好序,O(n * logn)
// 整体时间复杂度O(n * logn)
public static void heapSort2(int[] arr) {
int n = arr.length;
for (int i = n - 1; i >= 0; i--) {
heapify(arr, i, n);
}
int size = n;
while (size > 1) {
swap(arr, 0, --size);
heapify(arr, 0, size);
}
}
附上全部代码以及leetcode,牛客测试链接
// 堆结构和堆排序,填函数练习风格
// 测试链接 : https://leetcode.cn/problems/sort-an-array/
public class Code02_HeapSort {
public static int[] sortArray(int[] nums) {
if (nums.length > 1) {
// heapSort1为从顶到底建堆然后排序
// heapSort2为从底到顶建堆然后排序
// 用哪个都可以
// heapSort1(nums);
heapSort2(nums);
}
return nums;
}
// i位置的数,向上调整大根堆
// arr[i] = x,x是新来的!往上看,直到不比父亲大,或者来到0位置(顶)
public static void heapInsert(int[] arr, int i) {
while (arr[i] > arr[(i - 1) / 2]) {
swap(arr, i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
// i位置的数,变小了,又想维持大根堆结构
// 向下调整大根堆
// 当前堆的大小为size
public static void heapify(int[] arr, int i, int size) {
int l = i * 2 + 1;
while (l < size) {
// 有左孩子,l
// 右孩子,l+1
// 评选,最强的孩子,是哪个下标的孩子
int best = l + 1 < size && arr[l + 1] > arr[l] ? l + 1 : l;
// 上面已经评选了最强的孩子,接下来,当前的数和最强的孩子之前,最强下标是谁
best = arr[best] > arr[i] ? best : i;
if (best == i) {
break;
}
swap(arr, best, i);
i = best;
l = i * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 从顶到底建立大根堆,O(n * logn)
// 依次弹出堆内最大值并排好序,O(n * logn)
// 整体时间复杂度O(n * logn)
public static void heapSort1(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) {
heapInsert(arr, i);
}
int size = n;
while (size > 1) {
swap(arr, 0, --size);
heapify(arr, 0, size);
}
}
// 从底到顶建立大根堆,O(n)
// 依次弹出堆内最大值并排好序,O(n * logn)
// 整体时间复杂度O(n * logn)
public static void heapSort2(int[] arr) {
int n = arr.length;
for (int i = n - 1; i >= 0; i--) {
heapify(arr, i, n);
}
int size = n;
while (size > 1) {
swap(arr, 0, --size);
heapify(arr, 0, size);
}
}
}