🌅主页: f狐o狸x
文章目录
大家好!我是狐狸!上期内容我们完成了队列的实现,本期内容我们将开始学习数据结构中树的概念和堆的实现
一、树的概念及结构
1.1 树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
有一个特殊的结点,称为根结点,根节点没有前驱结点
除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
因此,树是递归定义的。
需要注意的是:树形结构中,子树之间不能有交集,否则就不是树形结构
1.2 树的概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林
1.3 树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法
等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
这个表示的方法又称为左孩子右兄弟表示法,狐狸我个人认为这是一个超级聪明的表示方法,可以想出这个表示方法的人真的是超级无敌聪明的人类,我们只需要定义一个结构体即可:
typedef int DataType;
struct Node
{
struct Node* _firstChild1;// 第一个孩子结点
struct Node* _pNextBrother;// 指向其下一个兄弟结点
DataType _data;// 结点中的数据域
}
例如这样的树,就可以用孩子兄弟表示法表示出来
真的太聪明了!用这样的的方法很快的就可以把这棵树遍历出来
1.4 树的应用
生活中最常见的树其实就是我们的目录
这里C盘就是根结点,每个文件都是一个子节点
二、二叉树概念及结构
认识了树之后,我们再来认识一下二叉树。
2.1 二叉树的概念
顾名思义,就是整个树的子节点只有<=2个
从上图不难看出:
1.二叉树不存在度大于2的结点
2.二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
这里其实也可以把二叉树理解为是一个执行了计划生育的树,他们的孩子结点只有0、1、2三个情况
现实中的二叉树
2.2 特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2k - 1,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.3 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
三、堆的实现
3.1 堆的概念及结构
1. 堆中某个节点的值总是不大于或不小于其父节点的值;
2. 堆总是一棵完全二叉树。
3.2 堆的实现
3.2.1 堆的创建
和通讯录一样,弄个顺序表的结构出来就好啦
typedef int HPDataType;
typedef struct Heap
{
HPDataType* Heap;
int size;
int capacity;
}HP;
3.2.2 堆的初始化和销毁
//堆的初始化
void HeapInit(HP* php)
{
assert(php);
php->Heap = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->Heap == NULL)
{
perror("HeapInit::malloc");
return;
}
php->capacity = 4;
php->size = 0;
}
//堆的销毁
void HeapDestory(HP* php)
{
assert(php);
free(php->Heap);
php->Heap = NULL;
}
3.2.3 堆的插入和向上调整算法
堆在插入和删除上有些些复杂,但是问题不大,我们来一步一步地分析,假设现在已经有了一个堆[ 3 8 5 10 11 7 ] 如图
此时,心血来潮的你打算插入一个1进去,那么,堆就会变成这样
聪明的你一定看出来了堆的顺序不对了,因此我们需要弄一个向上调整的函数
首先我们需要知道父节点的下标等于孩子结点的下标-1后再除以2
这个堆是小堆,所以谁小谁是爹,那我们就可以根据插入的数据的下标位置,算出他的父亲节点的位置,然后比大小,如果child小于parent,那就需要两个换位置,反之则符合小堆存储
酱紫不就搞定啦~代码如下:
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//向上调整
void AdjustUp(HPDataType* Heap, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (Heap[child] < Heap[parent])
{
swap(&Heap[child], &Heap[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆增加数据
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->capacity == php->size)
{
HPDataType* tmp = (HPDataType*)realloc(php->Heap, sizeof(HPDataType) * php->capacity * 2);
if (tmp == NULL)
{
perror("HeapPush::realloc");
return;
}
php->Heap = tmp;
php->capacity *= 2;
}
php->Heap[php->size] = x;
php->size++;
AdjustUp(php->Heap,php->size-1);
}
3.2.4 堆的删除和向下调整算法
这里的删除指的是删除堆顶元素,此时聪明的你肯定又能想到想顺序表那样后一个覆盖前一个就好啦,就像这样,又把上面的堆的堆顶元素删了,就像这样:
当你在沾沾自喜的发现自己好聪明的时候有没有发现,看似你把堆顶元素删了,但是你还顺手把堆的关系全毁了(例如:本来8和3是兄弟关系的,硬被你弄成父子了),而且这样的覆盖时间复杂度也高的一批
这样删那还不如别删,所以这里有出现了一种新的删除方法:
1.先把堆顶元素和最后一个元素交换位置,堆里的有效个数size-1
2.再用向下调整算法把堆的关系恢复
这样,我们就能轻松实现堆的插入删除了,代码如下:
//向下调整
void AdjustDown(HPDataType* Heap, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
//选出左右孩子中大的那个孩子
if (child + 1 < size && Heap[child + 1] > Heap[child])
{
child++;
}
if (Heap[parent] < Heap[child])
{
swap(&Heap[parent], &Heap[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆删除数据
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
swap(&php->Heap[0], &php->Heap[php->size - 1]);
php->size--;
AdjustDown(php->Heap,php->size,0);
}
3.2.5 堆的其他函数
其他的函数主要就是判断是否为空,返回堆顶元素,返回堆的大小
//返回堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
return php->Heap[0];
}
//判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//返回堆的大小
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
这样你就获得了一个堆,他可以帮你搞定很多事情
3.3 堆的应用
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
到这里,本期内容就全部结束啦,下期将会实现Top-K问题的解决和二叉树的构造
给个三连叭QAQ
✨这里是狐狸✨
❤️期待您的关注❤️