网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
既然上文说到可以直接把它看作二叉树,那不妨把逻辑结构画出来看看:
接下来,我们就要进行建堆了,有两种方法:
- 使用向上建堆,插入数据的思想建堆
- 使用向下调整建堆
💦向上调整建堆
**✨思路:**首先,**我们把第一个数字看成堆,也就是4,当第二个数字插入进去的时候,进行向上调整算法,使其确保为小堆,**向上调整的算法在上篇博文已详细讲解过,不过多赘述。具体插入数据过程就是遍历数组,确保数组里每一个数进行向上调整算法
✨:向上/下调整算法: 详解向上/下调整算法
✨:画图演示:(小根堆)
✨:代码演示
void swap(HPDatatype* x, HPDatatype* y) { int temp = 0; temp = *x; *x = *y; *y = temp; } void AdjustUp(HPDatatype* a, int child) { // 计算父亲的小标 int parent = (child - 1) / 2; // 当 child 的小标大于 0 就继续 (也就小于是根节点位置) while (child > 0) { // 小堆 < // 大堆 > if (a[child] < a[parent]) { swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } // 向上调整建堆 void Heap(int* a, int n) { //建堆 int i = 0; for (i = 1; i < n; i++) //应该从i=1时遍历,因为第一个数据在堆里不需要调整,后续再插入时调整 { AdjustUp(a, i); } printf("堆:\n"); for (int j = 0; j < n; j++) { printf("%d ", a[j]); } } int main() { // 建堆测试 int a[100]; int j = 0; int sum ; printf("请输入你要插入堆的数据(-1结束):>\n"); for (int i = 0; i < 100; i++) { scanf("%d", &sum); if (sum == -1) { break; } a[j++] = sum; } Heap(a, j); return 0; }
✨:效果演示:
符合小根堆的性质
💦向下调整建堆
- **问题:**能直接进行向下建堆吗?
答案:不能
解析:首先回顾下使用向下调整的前提是什么?必须得确保根结点的左右子树均为小堆才可,而这里,数组为乱序的,无法直接使用。
- 解决办法:从倒数第一个非叶结点开始向下调整,从下往上调
**✨:****分析:**从该解决方案中,我们首先要找到这个倒数第一个非叶结点的数在哪?其实最后一个结点的父亲即为倒数第一个非叶结点。当我们找到这个非叶结点时,把它和它的孩子看成一个整体,进行向下调整。调整后,再将次父节点向前挪动,再次向下调整,依次循环下去。
- 再回顾下父亲和孩子间的关系:
- leftchild = parent*2 + 1
- rightchild = parent*2 + 2
- parent = (child - 1) / 2
✨:画图分析
✨:代码演示:建小堆(数据:4 ,2,7,8,5,0,6)
void swap(HPDatatype* x, HPDatatype* y) { int temp = 0; temp = *x; *x = *y; *y = temp; } // 向下调整 // 向下调整的前提:后面的数据是堆 void AdjustDown(HPDatatype* a, int n, int parent) { // 左孩子 int child = parent * 2 + 1; // 孩子可能会超出数组范围 while (child < n) { // 如果右孩子小于左孩子 // 找出小的那个孩子 // 保证右孩子存在且不越界 // 大堆 > // 小堆 < if (child + 1 < n && a[child+1] < a[child]) { //跳转到有孩子那里 child++; } // 开始向下调整 // 大堆 > // 小堆 < if (a[child] < a[parent]) { swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } // 向下调整建堆 void Heap(int* a, int n) { //建堆 int i = 0; for (int i = ((n - 1) - 1) / 2; i >= 0; i--) { // 给定一个 a 数组,从 i 这个位置进行建堆, n 表示数组的大小 AdjustDown(a, n, i); } printf("堆:\n"); for (int j = 0; j < n; j++) { printf("%d ", a[j]); } } int main() { // 建堆测试 int a[100]; int j = 0; int sum ; printf("请输入你要插入堆的数据(-1结束):>\n"); for (int i = 0; i < 100; i++) { scanf("%d", &sum); if (sum == -1) { break; } a[j++] = sum; } Heap(a, j); return 0; }
✨:效果演示:
符合小根堆的性质
💦时间复杂度分析(向上建堆和向下建堆熟优?)
首先,我们画张图看下向上和向下建堆后的样子。
从上图中,我们可以看出,使用不同的方式建堆最后的样子是不同的,那哪种方式好呢?
**✨:**接下来,我将通过时间复杂度的方式为大家解惑:以一颗满二叉树为例:
✨****向上建堆的时间复杂度:
时间复杂度计算的是其调整的次数,根据上文的知识我们已经知晓其是从数组的第二个元素开始的,也就是可以理解为第二层的第一个节点。计算的思想非常简单:计算每层有多少个节点乘以该层的高度次,然后累计相加即可。如下:
通过计算得知:向上建堆的时间复杂度为O(N*logN)
✨****向下建堆的时间复杂度:
向下调整我们前面已经知道它是从倒数第1个非叶节点开始调整的,每层的调整次数为,该层的节点个数*该层高度减1,一直从第1层开始调直至倒数第2层,并将其依次累加,此计算过程和向上调整差不多,都是等比*等差的求和,过程如下:
通过计算得知:向下建堆的时间复杂度为****O(N)
通过上述计算,我们得到如下:
- 向上建堆:O(N*logN)
- 向下建堆:O(N)
由此可见,使用向下建堆的方式更优,其时间复杂度较小。当然,使用向上建堆也是可以的,只不过向下建堆更好一点。
💦升序(排序)建大堆还是小堆?
✨****思考:**排升序,建小堆可以吗?-- 可以是可以,但没啥意思。
首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第2小的数,不断重复上述过程……。建 n 个数的堆时间复杂度是O(N),****所以上述操作时间复杂度为O(N2),效率太低,**尤其是当数据量大的时候,效率更低,同时堆的价值没有被体现出来,还不如用直接排序。【最佳方法】排升序,因为数字越来越大,需要找到最大的数字,得建大堆
- 首先对 n 个数建大堆。
- 将最大的数(堆顶)和最后一个数交换,把最大的数放到最后。
- 前面 n-1 个数的堆结构没有被破坏(最后一个数不看做堆里面的),根节点的左右子树依旧是大堆,所以我们进行一次向下调整成大堆即可选出第2大的数,放到倒数第二个位置,然后重复上述步骤……。
【时间复杂度】:建堆时间复杂度为O(N),向下调整时间复杂度为O(logN),这里我们最多进行N-2次向下调整。
所以堆排序时间复杂度为 :O = O(N) + O(N-2)*O(logN) = O(N*logN),效率是很高的。
**✨解决方案:**升序建大堆,降序建小堆
💦完整的堆排序(升序)图形和代码演示
- 先看下建好大堆的样子:
- ✨思路:首先,得明确我们建堆后,此时堆顶就是最大的数据,现在我们把第一个数字和最后一个数字交换,把最后一个数字不看做堆里的,只需要数组个数N–即可。此时的左子树和右子树依旧是大堆,再进行向下调整即可。
- 画图解析过程:
- ✨:代码演示:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>// 数据类型存储
typedef int HPDatatype;// 数据交换
void Swap(HPDatatype* x, HPDatatype* y)
{
HPDatatype temp = 0;
temp = *x;
*x = *y;
*y = temp;
}// HPDatatype* a :表述动态数组
// n :表示堆内(数组内)的数据个数
// parent :父节点的下标(开始在根节点)
void AdjustDown(HPDatatype* a, int n, int parent)
{
// 左孩子
int child = parent * 2 + 1;
// 防止孩子跑出数组范围(最后一个节点为叶子节点)
while (child < n)
{
// 判断左右孩子那个大,将大的与父节点进行交换
// child+1 < n 表示确保右孩子存在
// 小堆 if (child + 1 < n && a[child] > a[child+1])
if (child + 1 < n && a[child] < a[child+1])
{
// 如果右孩子比左孩子大
child++;
}// 开始向下调整 // 小堆:if (a[child] < a[parent]) if (a[child] > a[parent]) // 大堆 { Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
{
Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else
[外链图片转存中…(img-S5i44Wdb-1715669928612)]
[外链图片转存中…(img-aGOUCmEh-1715669928613)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!