当前位置: 首页 > news >正文

【数据结构与算法】-二叉树(2):实现顺序结构二叉树(堆的实现),向上调整算法,向下调整算法,堆排序,TOP-K问题

【数据结构与算法】-二叉树(2):实现顺序结构二叉树(堆的实现),向上调整算法,向下调整算法,堆排序,TOP-K问题

目录
  • 实现顺序结构二叉树
    • 堆的概念与结构
    • 堆与二叉树的性质
    • 堆的实现
      • 定义堆结构
      • 堆的初始化,销毁,打印
      • 向上调整算法
      • 堆的插入
      • 向下调整算法
      • 堆的删除
      • 堆的判空
      • 取堆顶
    • 堆排序
      • 向上调整算法建堆
      • 向下调整算法建堆
      • 堆排序的实现
      • 堆排序时间复杂度计算
  • TOP-K问题
    • 造十万个数据
    • TOP-K代码
    • 空间足够的话

实现顺序结构二叉树

一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性

堆的概念与结构

如果有一个关键码的集合
K

{
k
0
,
k
1
,
k
2
,

,
k
n

1
}
K=/{k/_0, k/_1, k/2, /dots, k/{n-1}/}
K={k0​,k1​,k2​,…,kn−1​},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:
K
i

K
2
i
+
1
K/i/leq K/{2i+1}
Ki​≤K2i+1​且
K
i

K
2
i
+
2
K/i/leq K/{2i+2}
Ki​≤K2i+2​
(
K
i

K
2
i
+
1
(K/i/geq K/{2i+1}
(Ki​≥K2i+1​ 且
K
i

K
2
i
+
2
)
K/i/geq K/{2i+2})
Ki​≥K2i+2​)则称为小堆(或大堆)将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

堆与二叉树的性质

堆具有以下性质:

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。
    二叉树性质
    对于具有
    n
    n
    n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从
    0
    0
    0 开始编号,则对于序号为
    i
    i
    i 的结点有:

  • i

    0
    i>0
    i>0,
    i
    i
    i 位置结点的双亲序号:
    i

    1
    2
    /dfrac{i-1}{2}
    2i−1​(向下取整);
    i

    0
    i=0
    i=0,
    i
    i
    i 为根结点编号,无双亲结点。

  • 2
    i
    +
    1
    <
    n
    2i+1<n
    2i+1<n,左孩子序号:
    2
    i
    +
    1
    2i+1
    2i+1;
    2
    i
    +
    1

    n
    2i+1/geq n
    2i+1≥n 则无左孩子。

  • 2
    i
    +
    2
    <
    n
    2i+2<n
    2i+2<n,右孩子序号:
    2
    i
    +
    2
    2i+2
    2i+2;
    2
    i
    +
    2

    n
    2i+2/geq n
    2i+2≥n 则无右孩子。

堆的实现

堆底层结构为数组,因此定义堆的结构为:

  • 头文件
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> //定义堆结构 typedef int HPDataType; typedef struct Heap { HPDataType* arr; int size; //有效数据个数 int capaicty;//空间大小 }HP; void HPInit(HP* php); void HPDesTroy(HP* php); void HPPrint(HP* php); void Swap(int* x, int* y); //向下调整算法 void AdjustDown(HPDataType* arr, int parent, int n); //向上调整算法 void AdjustUp(HPDataType* arr, int child); void HPPush(HP* php, HPDataType x); void HPPop(HP* php); HPDataType HPTop(HP* php); bool HPEmpty(HP* php);
定义堆结构

因为底层选型是数组,跟之前的顺序表一样,因此不再赘述

//定义堆结构 typedef int HPDataType; typedef struct Heap { HPDataType* arr; int size; //有效数据个数 int capaicty;//空间大小 }HP;
堆的初始化,销毁,打印

不再赘述

#include"Heap.h" void HPInit(HP* php) { assert(php); php->arr = NULL; php->size = php->capaicty = 0; } void HPDesTroy(HP* php) { assert(php); if (php->arr) free(php->arr); php->arr = NULL; php->size = php->capaicty = 0; } void HPPrint(HP* php) { for (int i = 0; i < php->size; i++) { printf("%d ", php->arr[i]); } printf("/n"); } void Swap(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; }
向上调整算法

堆的插入
将新数据插入到数组的尾上,再进行向上调整算法,直到满足堆。
向上调整算法有一个前提:在新数据之上的左右子树必须是一个堆,才能调整。
向上调整算法

  • 先将元素插入到堆的末尾,即最后一个孩子之后
  • 插入之后如果堆的性质遭到破坏,将新插入结点顺着其双双亲往上调整到合适位置即可
//向上调整算法logn void AdjustUp(HPDataType* arr, int child) { int parent = (child - 1) / 2; while (child > 0) { //建大堆:> //建小堆: < if (arr[child] < arr[parent]) { Swap(&arr[child], &arr[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }

时间复杂的:logn

堆的插入

堆的插入

void HPPush(HP* php, HPDataType x) { assert(php); //空间不够要增容 if (php->size == php->capaicty) { //增容 int newCapacity = php->capaicty == 0 ? 4 : 2 * php->capaicty; HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType)); if (tmp == NULL) { perror("realloc fail!"); exit(1); } php->arr = tmp; php->capaicty = newCapacity; } //空间足够 php->arr[php->size] = x; //向上调整 AdjustUp(php->arr, php->size); ++php->size; }
向下调整算法

堆的删除(出堆)
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

向下调整算法有一个前提:要调整的堆顶之下的左右子树必须是一个堆,才能调整。
向下调整算法

  • 将堆顶元素与堆中最后一个元素进行交换
  • 删除堆中最后一个元素
  • 将堆顶元素向下调整到满足堆特性为止
//向下调整算法 logn void AdjustDown(HPDataType* arr, int parent, int n) { int child = parent * 2 + 1; while (child < n) { //建大堆:< //建小堆: > if (child + 1 < n && arr[child] > arr[child + 1]) { child++; } //孩子和父亲比较 //建大堆:> //建小堆:< if (arr[child] < arr[parent]) { Swap(&arr[child], &arr[parent]); parent = child; child = parent * 2 + 1; } else { break; } } }

时间复杂的:logn
parent:父结点,从哪里开始向下调整
n:调整范围的结点个数

堆的删除
void HPPop(HP* php) { assert(!HPEmpty(php)); Swap(&php->arr[0], &php->arr[php->size - 1]); --php->size; //堆顶数据需要向下调整 AdjustDown(php->arr, 0, php->size); }
堆的判空
bool HPEmpty(HP* php) { assert(php); return php->size == 0; }
取堆顶
HPDataType HPTop(HP* php) { assert(!HPEmpty(php)); return php->arr[0]; }

堆排序

随便给定一个乱序数组,用堆排序的思想解决

向上调整算法建堆

向上调整算法思想

//向上调整算法建堆O(n*logn) for (int i = 0; i < n; i++) { AdjustUp(arr, i); }

计算向上调整算法建堆时间复杂度
向上调整建堆 错位相减完整推导

原式①:

T
(
h
)

2
1

1
+
2
2

2
+
2
3

3
+

+
2
h

2

(
h

2
)
+
2
h

1

(
h

1
)
(①)
T(h)=2^1 /cdot 1+2^2 /cdot 2+2^3 /cdot 3+/dots+2^{h-2} /cdot (h-2)+2^{h-1} /cdot (h-1) /tag{①}
T(h)=21⋅1+22⋅2+23⋅3+⋯+2h−2⋅(h−2)+2h−1⋅(h−1)(①)
同乘 2 得式②:

2
T
(
h
)

2
2

1
+
2
3

2
+
2
4

3
+

+
2
h

1

(
h

2
)
+
2
h

(
h

1
)
(②)
2T(h)=2^2 /cdot 1+2^3 /cdot 2+2^4 /cdot 3+/dots+2^{h-1} /cdot (h-2)+2^h /cdot (h-1) /tag{②}
2T(h)=22⋅1+23⋅2+24⋅3+⋯+2h−1⋅(h−2)+2h⋅(h−1)(②)

− ① 错位相减:

2
T
(
h
)

T
(
h
)


2
1

1

2
2

2
3



2
h

1
+
2
h
(
h

1
)
T
(
h
)

2
h
(
h

1
)

(
2
1
+
2
2
+
2
3
+

+
2
h

1
)
/begin{aligned} 2T(h)-T(h) &= -2^1/cdot1 - 2^2 - 2^3 - /dots - 2^{h-1} + 2^h(h-1) // T(h) &= 2^h(h-1) - /big(21+22+23+/dots+2{h-1}/big) /end{aligned}
2T(h)−T(h)T(h)​=−21⋅1−22−23−⋯−2h−1+2h(h−1)=2h(h−1)−(21+22+23+⋯+2h−1)​
等比数列求和:

2
1
+
2
2
+

+
2
h

1

2
h

2
21+22+/dots+2^{h-1} = 2^h - 2
21+22+⋯+2h−1=2h−2
代入化简:

T
(
h
)

2
h
(
h

1
)

(
2
h

2
)

h

2
h

2
h

2
h
+
2

h

2
h

2
h
+
1
+
2
/begin{aligned} T(h) &= 2^h(h-1) - (2^h - 2) // &= h/cdot 2^h - 2^h - 2^h + 2 // &= h/cdot 2^h - 2^{h+1} + 2 /end{aligned}
T(h)​=2h(h−1)−(2h−2)=h⋅2h−2h−2h+2=h⋅2h−2h+1+2​

T
(
h
)

2
h
(
h

2
)
+
2
T(h) = 2^h(h-2) + 2
T(h)=2h(h−2)+2
根据二叉树的性质:

n

2
h

1
,
h

log

2
(
n
+
1
)
n = 2^h - 1,/quad h = /log/_2(n+1)
n=2h−1,h=log2​(n+1)
把h换形成n,注意这里变换对应法则,不换自变量(T(h)不将h换成n)

F
(
n
)

(
n
+
1
)
(
log

2
(
n
+
1
)

2
)
+
2
F(n) = (n+1)/big(/log/_2(n+1) - 2/big) + 2
F(n)=(n+1)(log2​(n+1)−2)+2

F
(
n
)

O
(
n
log

2
n
)
F(n) = O(n/log/_2 n)
F(n)=O(nlog2​n)

向下调整算法建堆

向下调整算法思想

//向下调整算法——建堆O(n) for (int i = (n - 1 - 1) / 2; i >= 0; i--) { AdjustDown(arr, i, n); }

计算向下调整算法建堆时间复杂度

第1层,
2
0
2^0
20 个结点,需要向下移动
h

1
h-1
h−1 层
第2层,
2
1
2^1
21 个结点,需要向下移动
h

2
h-2
h−2 层
第3层,
2
2
2^2
22 个结点,需要向下移动
h

3
h-3
h−3 层
第4层,
2
3
2^3
23 个结点,需要向下移动
h

4
h-4
h−4 层


/dots


h

1
h-1
h−1层,
2
h

2
2^{h-2}
2h−2 个结点,需要向下移动
1
1
1 层
需要移动结点总的移动步数为:每层结点个数
×
/times
× 向下调整次数
T
(
h
)

2
0
(
h

1
)
+
2
1
(
h

2
)
+
2
2
(
h

3
)
+
2
3
(
h

4
)
+

+
2
h

3

2
+
2
h

2

1
T(h)=2^0 (h-1)+2^1 (h-2)+2^2 (h-3)+2^3 (h-4)+/dots+2^{h-3} /cdot 2+2^{h-2} /cdot 1
T(h)=20(h−1)+21(h−2)+22(h−3)+23(h−4)+⋯+2h−3⋅2+2h−2⋅1
同乘 2:

2
T
(
h
)

2
1
(
h

1
)
+
2
2
(
h

2
)
+
2
3
(
h

3
)
+
2
4
(
h

4
)
+

+
2
h

2

2
+
2
h

1

1
2T(h)=21(h-1)+22(h-2)+23(h-3)+24(h-4)+/dots+2^{h-2}/cdot 2+2^{h-1}/cdot 1
2T(h)=21(h−1)+22(h−2)+23(h−3)+24(h−4)+⋯+2h−2⋅2+2h−1⋅1
错位相减:

T
(
h
)

1

h
+
2
1
+
2
2
+
2
3
+
2
4
+

+
2
h

2
+
2
h

1
T(h)=1-h+21+22+23+24+/dots+2{h-2}+2{h-1}
T(h)=1−h+21+22+23+24+⋯+2h−2+2h−1
整理:

T
(
h
)

2
0
+
2
1
+
2
2
+
2
3
+
2
4
+

+
2
h

2
+
2
h

1

h
T(h)=20+21+22+23+24+/dots+2{h-2}+2^{h-1}-h
T(h)=20+21+22+23+24+⋯+2h−2+2h−1−h
等比数列求和化简:

T
(
h
)

2
h

1

h
T(h)=2^h-1-h
T(h)=2h−1−h
根据二叉树的性质:

n

2
h

1
,
h

log

2
(
n
+
1
)
n=2^h-1,/quad h=/log/_2(n+1)
n=2h−1,h=log2​(n+1)
代入得:

F
(
n
)

n

log

2
(
n
+
1
)

n
F(n)=n-/log/_2(n+1) /approx n
F(n)=n−log2​(n+1)≈n
向下调整算法建堆时间复杂度为:

O
(
n
)
O(n)
O(n)

堆排序的实现

数组建堆,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次大的数据

//堆排序————使用的是堆结构的思想 n * logn void HeapSort(int* arr, int n) { //向下调整算法——建堆O(n) for (int i = (n - 1 - 1) / 2; i >= 0; i--) { AdjustDown(arr, i, n); } ////向上调整算法建堆O(n*logn) //for (int i = 0; i < n; i++) //{ // AdjustUp(arr, i); //} //n*logn int end = n - 1; while (end > 0) { Swap(&arr[0], &arr[end]); AdjustDown(arr, 0, end);//logn end--; } }
堆排序时间复杂度计算

通过分析发现,堆排序第二个循环中的向下调整,与建堆中的向上调整算法时间复杂度计算一致(也是求和),此处不再赘述。(建堆默认向下调整思想
因此,堆排序总的时间复杂度为:

O
(
n
+
n
log

n
)
O(n + n/log n)
O(n+nlogn)
化简可得堆排序最终时间复杂度:

O
(
n
log

n
)
O(n/log n)
O(nlogn)
TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。
最佳的方式就是用堆来解决,基本思路如下:

1)用数据集合中前K个元素来建堆

  • 前k个最大的元素,则建小堆 (因为堆顶永远是最小的,通过排出堆顶小元素,存住堆顶下面的大元素,即可用有限的空间找出海量大数据的小元素)
  • 前k个最小的元素,则建大堆

2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

造十万个数据

void CreateNDate() { // 造数据 int n = 100000; srand(time(0)); const char* file = "data.txt"; FILE* fin = fopen(file, "w"); if (fin == NULL) { perror("fopen error"); return; } for (int i = 0; i < n; ++i) { int x = (rand() + i) % 1000000; fprintf(fin, "%d/n", x); } fclose(fin); }




TOP-K代码

void TopK() { int k = 0; printf("请输入K:"); scanf("%d", &k); const char* file = "data.txt"; FILE* fout = fopen(file, "r"); if (fout == NULL) { perror("fopen fail!"); exit(1); } //申请空间大小为k的整型数组 int* minHeap = (int*)malloc(sizeof(int) * k); if (minHeap == NULL) { perror("malloc fail!"); exit(2); } //读取文件中k个数据放到数组中 for (int i = 0; i < k; i++) { fscanf(fout, "%d", &minHeap[i]); } //数组调整建堆--向下调整建堆 //找最大的前K个数,建小堆 for (int i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDown(minHeap, i, k); } //遍历剩下的n-k个数,跟堆顶比较,谁大谁入堆 int data = 0; while (fscanf(fout, "%d", &data) != EOF) { if (data > minHeap[0]) { minHeap[0] = data; AdjustDown(minHeap, 0, k); } } //打印堆里的数据 for (int i = 0; i < k; i++) { printf("%d ", minHeap[i]); } printf("/n"); fclose(fout); }


时间复杂度:(默认向下调整)

O
(
n
)

k
+
(
n

k
)
log

2
k
O(n) = k + (n-k)/log/_2 k
O(n)=k+(n−k)log2​k

空间足够的话

O
(
n
log

k
)
O(n/log k)
O(nlogk) 看似永远优于全部建堆排序:
O
(
n
log

n
)
O(n/log n)
O(nlogn)
其实不然,仔细学习我前面的思路你会发现,堆排序浪费时间的地方其实是出堆,而全部建堆排序只需要出堆
K
K
K次,所以

n
+
k
log

n



n
log

k
/boldsymbol{n + k/log n /;/ll/; n/log k}
n+klogn≪nlogk
时间 :全部建堆排序


/ll/;
≪ TOP-K
所以在空间无限,全局建堆未尝不可!!!

《网络安全从零到精通全套学习大礼包》

96节从入门到精通的全套视频教程免费领取

如果你也想通过学网络安全技术去帮助就业和转行,我可以把我自己亲自录制的96节 从零基础到精通的视频教程以及配套学习资料无偿分享给你。

网络安全学习路线图

想要学习 网络安全,作为新手一定要先按照路线图学习方向不对,努力白费。对于从来没有接触过网络安全的同学,我帮大家准备了从零基础到精通学习成长路线图以及学习规划。可以说是最科学最系统的学习路线,大家跟着这个路线图学习准没错。

配套实战项目/源码

所有视频教程所涉及的实战项目和项目源码

学习电子书籍

学习网络安全必看的书籍和文章的PDF,市面上网络安全书籍确实太多了,这些是我精选出来的

面试真题/经验

以上资料如何领取?

) 看似永远优于全部建堆排序:
O
(
n
log

n
)
O(n/log n)
O(nlogn)
其实不然,仔细学习我前面的思路你会发现,堆排序浪费时间的地方其实是出堆,而全部建堆排序只需要出堆
K
K
K次,所以

n
+
k
log

n



n
log

k
/boldsymbol{n + k/log n /;/ll/; n/log k}
n+klogn≪nlogk
时间 :全部建堆排序


/ll/;
≪ TOP-K
所以在空间无限,全局建堆未尝不可!!!

《网络安全从零到精通全套学习大礼包》

96节从入门到精通的全套视频教程免费领取

如果你也想通过学网络安全技术去帮助就业和转行,我可以把我自己亲自录制的96节 从零基础到精通的视频教程以及配套学习资料无偿分享给你。

网络安全学习路线图

想要学习 网络安全,作为新手一定要先按照路线图学习方向不对,努力白费。对于从来没有接触过网络安全的同学,我帮大家准备了从零基础到精通学习成长路线图以及学习规划。可以说是最科学最系统的学习路线,大家跟着这个路线图学习准没错。

配套实战项目/源码

所有视频教程所涉及的实战项目和项目源码

学习电子书籍

学习网络安全必看的书籍和文章的PDF,市面上网络安全书籍确实太多了,这些是我精选出来的

面试真题/经验

以上资料如何领取?

http://www.jsqmd.com/news/853548/

相关文章:

  • KUKA 虚拟调试实战——打通Simpro4.1与OfficeLite8.6的通信与实时控制链路
  • 使用Taotoken后我的大模型API延迟与稳定性体感观察
  • RoCEv2网络性能调优笔记:当你的AI训练集群遇到网络拥塞时,PFC和ECN到底谁先干活?
  • MIUI手机管家自动任务还能这么玩?手把手教你用备用机+智能插座实现远程打卡(附详细避坑指南)
  • SAR船舶检测数据集(SSDD)终极指南:从入门到精通
  • 2026年5月最新 30米量程一体式超声波液位计十大品牌评选 - 液体流量液位品牌推荐
  • 为OpenClaw智能体工作流配置Taotoken作为稳定的后端大模型服务提供商
  • VMware Unlocker 4.2.7终极指南:在非苹果硬件上高效运行macOS虚拟机
  • Taotoken API Key管理功能实现团队权限与访问控制
  • Faster-Whisper-GUI日语语音识别优化指南:3个关键技巧解决你的日语转写难题
  • 从AMBA1到AMBA5:20年演进,一文看懂ARM总线如何重塑SoC设计
  • 【天津线下、IEEE出版、连续五届EI检索】第六届控制与智能机器人国际学术会议(ICCIR 2026)
  • 告别杂乱排版!用ArcGIS Desktop布局工具,从零制作一张专业地图海报(附模板)
  • AI教材生成神器,低查重保障,快速产出符合要求的专业教材!
  • 用 NerfStudio 快速重建手办/商品:从手机拍照到 3D 模型的完整流程
  • Amphenol ICC DRPC11A005040工业线束组件解析
  • Insyde BIOS高级设置解锁工具:从基础配置到专业调优的完整指南
  • 从Arduino到Linux主机:用C++和termios.h给你的树莓派/香橙派写个串口调试助手
  • 空间约束化学气相沉积:精准调控硬碳微观结构,赋能高性能碱金属离子电池负极
  • 黎阳之光:以视频孪生重构智慧医院信息化,打造高标项目核心竞争力
  • 从六边形到六阶梯波:深入浅出图解SVPWM过调制算法的两个关键阶段
  • 如何用Autovisor智慧树刷课工具解放你的学习时间?完整指南来了!
  • 从一次Monstra文件上传绕过,聊聊安全研究员如何高效“刷”Vulfocus靶场(含CVE-2020-13384复现笔记)
  • 2026年海南海口三亚食品经营许可证办理服务商哪家强?海南财税办理服务商客户口碑权威排行榜,助力高效拿证! - GrowthUME
  • 你的Delay和串口为啥老出问题?STM32F103芯片移植后的时钟与晶振调试指南
  • 谷歌外链怎么发?机械B2B找对口链接的3个绝招
  • 从HFSS到FDTD:微带线特性阻抗仿真实战与Matlab实现对比
  • 拆解两款低压MOS芯片:4606和8205A,实测驱动电压低至0.7V,低压电路神器?
  • Perplexity发音查询功能实测对比:5大AI工具发音准确率、延迟、多语种支持度全维度评测(附测试数据)
  • RK3562J AMP双系统裸核中断嵌套实战:从原理到代码实现