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

手撕⼆叉树——堆

1.

1.1 树的概念与结构

树是⼀种⾮线性的数据结构,它是由 n n>=0 ) 个有限结点组成⼀个具有层次关系的集合。把它
叫做 树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。
有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1 T2 …… Tm ,其中每⼀个集合
  Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有 0
个或多个后继。因此,树是递归定义的。
树形结构中,⼦树之间不能有交集,否则就不是树形结构
⾮树形结构:
⼦树是不相交的(如果存在相交就是图了,图以后得课程会有讲解)
除了根结点外,每个结点有且仅有⼀个⽗结点
⼀棵N个结点的树有N-1条边

1.2 树相关术语

⽗结点/双亲结点 :若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点; 如上图:A是B                                  的⽗结点
⼦结点/孩⼦结点 :⼀个结点含有的⼦树的根结点称为该结点的⼦结点; 如上图:B是A的孩⼦结点
结点的度 :⼀个结点有⼏个孩⼦,他的度就是多少;⽐如A的度为6,F的度为2,K的度为0
树的度:⼀棵树中,最⼤的结点的度称为树的度; 如上图:树的度为 6
叶⼦结点/终端结点:度为 0 的结点称为叶结点; 如上图: B C H I... 等结点为叶结点
分⽀结点/⾮终端结点:度不为 0 的结点; 如上图: D E F G... 等结点为分⽀结点
兄弟结点:具有相同⽗结点的结点互称为兄弟结点(亲兄弟); 如上图: B C 是兄弟结点
结点的层次:从根开始定义起,根为第 1 层,根的⼦结点为第 2 层,以此类推;
树的⾼度或深度:树中结点的最⼤层次; 如上图:树的⾼度为 4
结点的祖先:从根到该结点所经分⽀上的所有结点;如上图: A 是所有结点的祖先
路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列;⽐如A到Q的路径             为: A-E-J-Q;H到Q的路径H-D-A-E-J-Q
⼦孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。如上图:所有结点都是A的⼦孙
森林:由 m m>0 ) 棵互不相交的树的集合称为森林;

1.3 树的表⽰

孩⼦兄弟表⽰法:
树结构相对线性表就⽐较复杂了,要存储表⽰起来就⽐较⿇烦了,既然保存值域,也要保存结点和
结 点之间的关系,实际中树有很多种表⽰⽅式如:双亲表⽰法,孩⼦表⽰法、孩⼦双亲表⽰法以
及孩⼦ 兄弟表⽰法等。我们这⾥就简单的了解其中最常⽤的孩⼦兄弟表⽰法
struct TreeNode
{struct Node* child; // 左边开始的第⼀个孩⼦结点struct Node* brother; // 指向其右边的下⼀个兄弟结点int data; // 结点中的数据域
};

1.4 树形结构实际运⽤场景

⽂件系统是计算机存储和管理⽂件的⼀种⽅式,它利⽤树形结构来组织和管理⽂件和⽂件夹。在⽂
件 系统中,树结构被⼴泛应⽤,它通过⽗结点和⼦结点之间的关系来表⽰不同层级的⽂件和⽂件
夹之间 的关联。

2. ⼆叉树

2.1 概念与结构

在树形结构中,我们最常⽤的就是⼆叉树,⼀棵⼆叉树是结点的⼀个有限集合,该集合由⼀个根结
点加上两棵别称为左⼦树和右⼦树的⼆叉树组成或者为空

从上图可以看出⼆叉树具备以下特点:
1. ⼆叉树不存在度⼤于 2 的结点
2. ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树
注意:对于任意的⼆叉树都是由以下⼏种情况复合⽽成的
现实中的⼆叉树

2.2 特殊的⼆叉树

2.2.1 满⼆叉树
⼀个⼆叉树,如果每⼀个层的结点数都达到最⼤值,则这个⼆叉树就是满⼆叉树。也就是说,如果
⼀ 个⼆叉树的层数为 K ,且结点总数是 2 k − 1 ,则它就是满⼆叉树。
2.2.2 完全⼆叉树
完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。对于深度为 K 的,有 n
个结点的⼆叉树,当且仅当其每⼀个结点都与深度为K的满⼆叉树中编号从 1 n 的结点⼀⼀对
应时称之为完全⼆叉树。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树.
💡 ⼆叉树性质
        根据满⼆叉树的特点可知:
        1)若规定根结点的层数为 1 ,则⼀棵⾮空⼆叉树的第i层上最多有 2 i −1 个结点
        2)若规定根结点的层数为 1 ,则深度为 h 的⼆叉树的最⼤结点数是 2 h − 1
        3)若规定根结点的层数为 1 ,具有 n 个结点的满⼆叉树的深度 ( log
              以2为底, n+1 为对数)

2.3 ⼆叉树存储结构

2.3.1 顺序结构
顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会
有空间的浪费,完全⼆叉树更适合使⽤顺序结构存储。

现实中我们通常把堆(⼀种⼆叉树)使⽤顺序结构的数组来存储,需要注意的是这⾥的堆和操作系
统 虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域
分段。
2.3.2 链式结构
⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的
⽅法 是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩
⼦和右孩 ⼦所在的链结点的存储地址 。链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都
是⼆叉链。后 ⾯课程学到⾼阶数据结构如红⿊树等会⽤到三叉链。

3. 实现顺序结构⼆叉树

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

3.1 堆的概念与结构

如果有⼀个关键码的集合 ,把它的所有元素按完全⼆叉树的顺序存储⽅ 式存储,在⼀个⼀维数组
中,并满⾜: ( 且 ), i = 0、 1 2... ,则称为⼩堆(或⼤堆)。将根结点最⼤的堆叫做最⼤堆或⼤
根堆,根结点最⼩的堆叫做最⼩堆或⼩根堆。
堆具有以下性质
堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值;
堆总是⼀棵完全⼆叉树。
💡 ⼆叉树性质
对于具有 n 个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从
  0 开始编号,则对于序号为 i 的结点有:
1. i>0 i 位置结点的双亲序号: (i-1)/2 i=0 i 为根结点编号,⽆双亲结点
2. 2i+1<n ,左孩⼦序号: 2i+1 2i+1>=n 否则⽆左孩⼦
3. 2i+2<n ,右孩⼦序号: 2i+2 2i+2>=n 否则⽆右孩⼦

3.2 堆的实现

堆底层结构为数组,因此定义堆的结构为:
Heap.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>//结构体  堆  ---数组
typedef int HPDataType;
typedef struct Heap
{HPDataType* arr;int size;//有效数据个数int capacity;//空间大小
}HP;//初始化
void HPInit(HP* php);//销毁
void HPDestroy(HP* php);
//向上调整
void	AddjustUp(HPDataType* arr,int size);
//插入数据
void HPPush(HP* php, HPDataType x);
//删除数据
void HPPop(HP* php);
// 判空
bool HPEmpty(HP* php);
//打印堆顶数据
HPDataType HPTop(HP* php);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void test01()
{HP hp;HPInit(&hp);int arr[] = { 15,13,10,20,17,19 };for (int i = 0; i < 6; i++){HPPush(&hp, arr[i]);}while (!HPEmpty(&hp)){printf("%d ", HPTop(&hp));HPPop(&hp);}HPDestroy(&hp);
}
int main()
{test01();return 0;
}

Heap.c

初始化

void HPInit(HP* php) {assert(php);php->arr = NULL;php->size = php->capacity = 0;
}

插入数据

//交换
void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y= tmp;
}
//向上调整
void	AddjustUp(HPDataType* arr, int size)
{int parent = (size - 1) / 2;while (size>=0){if (arr[size] < arr[parent]){Swap(&arr[size], &arr[parent]);size = parent;parent = (size - 1) / 2;}else{break;}}
}
void HPPush(HP* php, HPDataType x)
{assert(php);if (php->capacity == php->size){int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* tmp = (HP*)realloc(php->arr, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}php->arr = tmp;php->capacity = newcapacity;}php->arr[php->size] = x;AddjustUp(php->arr, php->size);++php->size;
}

删除数据

void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y= tmp;
}
void AddjustDown(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;}}
}
void HPPop(HP* php)
{assert(php && php->size);Swap(&php->arr[0], &php->arr[php->size - 1]);--php->size;AddjustDown(php->arr, 0, php->size);
}

判空

bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}

打印堆顶数据

HPDataType HPTop(HP* php)
{assert(php && php->size);return php->arr[0];
}
3.2.1 向上调整算法
堆的插⼊
将新数据插⼊到数组的尾上,再进⾏向上调整算法,直到满⾜堆。
💡 向上调整算法
先将元素插⼊到堆的末尾,即最后⼀个孩⼦之后
插⼊之后如果堆的性质遭到破坏,将新插⼊结点顺着其双双亲往上调整到合适位置即可
 void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;while(child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}
}
void HPPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size-1);
}

计算向上调整算法建堆时间复杂度
因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个结点不影响最终结果)
分析:
第1层, 2 0 个结点,需要向上移动0层
第2层, 2 1 个结点,需要向上移动1层
第3层, 2 2 个结点,需要向上移动2层
第4层, 2 3 个结点,需要向上移动3层
......
第h层, 2 h −1 个结点,需要向上移动h-1层
则需要移动结点总的移动步数为:每层结点个数 * 向上调整次数(第⼀层调整次数为0)
T ( h ) = 2 1 ∗ 1 + 2 2 ∗ 2 + 2 3 ∗ 3 + .. + 2 h −2 ∗ ( h − 2) + 2 h −1 ∗ ( h − 1)
2 ∗ T ( h ) = 2 2 ∗ 1 + 2 3 ∗ 2 + 2 4 ∗ 3 + .. + 2 h −1 ∗ ( h − 2) + 2 h ∗ ( h − 1)
② ⼀ ① 错位相减:
T ( h ) = −2 1 ∗ 1 − (2 2 + 2 3 + .. + 2 h −2 + 2 h −1 ) + 2 h ∗ ( h − 1)
T ( h ) = −2 0 − 2 1 ∗ 1 − (2 2 + 2 3 + .. + 2 h −2 + 2 h −1 ) + 2 h ∗ ( h − 1) + 2 0
T ( h ) = −(2 0 + 2 1 ∗ 1 + 2 2 + 2 3 + .. + 2 h −2 + 2 h −1 ) + 2 h ∗ ( h − 1) + 2 0
T ( h ) = −(2 h − 1) + 2 h ∗ ( h − 1) + 2 0
T ( h ) = −(2 h − 1) + 2 h ∗ ( h − 1) + 2 0
根据⼆叉树的性质: n = 2 h − 1 h = log 2 ( n + 1)
T ( n ) = − N + 2 h ∗ ( h − 1) + 2 0
F ( h ) = 2 h ( h − 2) + 2
F ( n ) = ( n + 1)(log 2 ( n + 1) − 2) + 2
由此可得:
💡 向上调整算法建堆时间复杂度为: O ( n ∗ log 2 n )
3.2.2 向下调整算法
堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进⾏向下调整算法。
向下调整算法有⼀个前提:左右⼦树必须是⼀个堆,才能调整。
💡 向下调整算法
        • 将堆顶元素与堆中最后⼀个元素进⾏交换
        • 删除堆中最后⼀个元素
        • 将堆顶元素向下调整到满⾜堆特性为⽌
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 HPPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}
计算向下调整算法建堆时间复杂度
分析:
第1层, 2 0 个结点,需要向下移动h-1层
第2层, 2 1 个结点,需要向下移动h-2层
第3层, 2 2 个结点,需要向下移动h-3层
第4层, 2 3 个结点,需要向下移动h-4层
......
第h-1层, 2 h −2 个结点,需要向下移动1层
则需要移动结点总的移动步数为:每层结点个数 * 向下调整次数
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
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
② ⼀ ① 错位相减:
T ( h ) = 1 − h + 2 1 + 2 2 + 2 3 + 2 4 + .. + 2 h −2 + 2 h −1
T ( h ) = 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + . + 2 h −2 + 2 h −1 h
T ( h ) = 2 h − 1 − h
根据⼆叉树的性质: n = 2 h − 1 h = log 2 ( n + 1)
T ( n ) = n log 2 ( n + 1) ≈ n
💡 向下调整算法建堆时间复杂度为: O ( n )

3.3 堆的应⽤

3.3.1 堆排序
版本⼀:基于已有数组建堆、取堆顶元素完成排序版本
// 1、需要堆的数据结构
// 2、空间复杂度 O(N)
void HeapSort(int* a, int n)
{HP hp;for(int i = 0; i < n; i++){HPPush(&hp,a[i]);}int i = 0;while (!HPEmpty(&hp)){a[i++] = HPTop(&hp);HPPop(&hp);}HPDestroy(&hp);
}
该版本有⼀个前提,必须提供有现成的数据结构堆
版本⼆:数组建堆,⾸尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次⼤的数据
// 升序,建⼤堆
// 降序,建⼩堆
// O(N*logN)
void HeapSort(int* a, int n)
{// a数组直接建堆 O(N)for (int i = (n-1-1)/2; i >= 0; --i){AdjustDown(a, n, i);}// O(N*logN)int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}
堆排序时间复杂度计算
分析:
第1层, 个结点,交换到根结点后,需要向下
移动0层
2 0
第2层, 个结点,交换到根结点后,需要向下
移动1层
2 1
第3层, 个结点,交换到根结点后,需要向下
移动2层
2 2
第4层, 个结点,交换到根结点后,需要向下
移动3层
2 3
......
第h层, 个结点,交换到根结点后,需要向
下移动h-1层
通过分析发现,堆排序第⼆个循环中的向下调整与建堆中的向上调整算法时间复杂度计算⼀致,此处不再赘述。因此,堆排序的时间复杂度为 O ( n + n ∗ log n ) ,即 O ( n log n )
💡 堆排序时间复杂度为: O ( n log n )
3.3.2 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);
}
void topk()
{printf("请输⼊k:>");int k = 0;scanf("%d", &k);const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen error");return;}int val = 0;int* minheap = (int*)malloc(sizeof(int) * k);if (minheap == NULL){perror("malloc error");return;}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, k, i);}int x = 0;while (fscanf(fout, "%d", &x) != EOF){// 读取剩余数据,⽐堆顶的值⼤,就替换他进堆if (x > minheap[0]){minheap[0] = x;AdjustDown(minheap, k, 0);}}for (int i = 0; i < k; i++){printf("%d ", minheap[i]);}fclose(fout);
}
时间复杂度: O ( n ) = k + ( n k )log 2 k

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • C语言实现Reactor
  • Flask条件查询接口出现SQL注入,使用参数化查询:写法的解决方案(附带企业级开发实际例子与经验分享)
  • java基础 之 常用遍历方法
  • Spring DI 数据类型—— set 方法注入
  • 达梦数据库的系统视图v$db_cache
  • Elasticsearch DSL 语法详解
  • 【Qt】输入类控件QLineEdit
  • 电连接器的质量等级选择
  • 通用人工智能不应该完全以人类为标准
  • Adobe After Effects的插件--------CC Cylinder
  • ESP32 分区表介绍
  • 通配符证书:轻松管理您的子域名安全
  • Java中实现一个定时任务并在特定时刻弹出窗口提醒用户需要放松休息
  • 大模型19:微调大模型方法
  • 《黑神话.悟空》:一场跨越神话与现实的深度探索
  • 【干货分享】SpringCloud微服务架构分布式组件如何共享session对象
  • 30秒的PHP代码片段(1)数组 - Array
  • CAP理论的例子讲解
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • Git初体验
  • GraphQL学习过程应该是这样的
  • IDEA常用插件整理
  • JDK 6和JDK 7中的substring()方法
  • js ES6 求数组的交集,并集,还有差集
  • Leetcode 27 Remove Element
  • Octave 入门
  • orm2 中文文档 3.1 模型属性
  • v-if和v-for连用出现的问题
  • vue--为什么data属性必须是一个函数
  • 从@property说起(二)当我们写下@property (nonatomic, weak) id obj时,我们究竟写了什么...
  • 二维平面内的碰撞检测【一】
  • 翻译--Thinking in React
  • 复习Javascript专题(四):js中的深浅拷贝
  • 和 || 运算
  • 理解 C# 泛型接口中的协变与逆变(抗变)
  • 力扣(LeetCode)357
  • 使用 Xcode 的 Target 区分开发和生产环境
  • Android开发者必备:推荐一款助力开发的开源APP
  • ​比特币大跌的 2 个原因
  • !!【OpenCV学习】计算两幅图像的重叠区域
  • # 移动硬盘误操作制作为启动盘数据恢复问题
  • #Datawhale X 李宏毅苹果书 AI夏令营#3.13.2局部极小值与鞍点批量和动量
  • (2024.6.23)最新版MAVEN的安装和配置教程(超详细)
  • (LeetCode) T14. Longest Common Prefix
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (pojstep1.1.2)2654(直叙式模拟)
  • (四)React组件、useState、组件样式
  • (四)模仿学习-完成后台管理页面查询
  • ../depcomp: line 571: exec: g++: not found
  • ./configure,make,make install的作用
  • ./和../以及/和~之间的区别
  • .net wcf memory gates checking failed
  • .net 使用$.ajax实现从前台调用后台方法(包含静态方法和非静态方法调用)
  • .net通用权限框架B/S (三)--MODEL层(2)
  • /etc/sudoers (root权限管理)