前言:前面我们学习了栈和队列。今天我们来学习一种新的数据结构---------树。
首先我们来了解一下树的概念。
1.树的概念与结构
前面我们学习过的顺序表,栈都是一种顺序结构。链表,队列是链式结构 。今天学习的树也是一种链式结构。它是由n(n>=0)个有限节点组成一个具有层次关系的集合。把它叫做树,是因为它看起来像是一棵倒挂的树,也就是说它的根朝上,叶朝下。
.
有一个特殊的节点,称作根节点,没有前驱结点
。
.
除根节点外,其余节点被分成M(M>0)个互不相交的集合T1,T2,T3...Tm,其中每一个集合Ti(1<=i<=m)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继节点,因此树是递归定义的
。
.
子树是不相交的
。
.
除了根节点,每个节点有且仅有一个父节点
。
.
一棵N个节点的树有N-1条边
。
树形结构
非树形结构
2. 树的相关术语
父结点/双亲结点 :若一个结点含有子结点,则这个结点称为其子结点的父结点
。(A是B的父结点)
子结点/孩子结点 :一个结点含有子树的根结点称为该结点的子结点
。(如上图:B是A的子结点)
结点的度 :一个结点有几个孩子,该结点的度就是多少
。
树的度 :一棵树中,最大结点的度就是树的度
。
叶子结点/终端结点 :度为0的结点称为叶子结点
。(如J,K,L)
分支结点/非终端结点 :度不为0的结点
。(B,C,D等)
兄弟结点 :具有相同父结点的结点互称为兄弟结点
。(B,C,D)
结点的层次 :从根结点开始定义,根结点为第一层,以此类推
。
树的高度或深度 :树中结点的最大层次
。
结点的祖先 :从根到该结点所经分支上所有的结点
。
路径 :一条从树中任意结点出发,沿父结点-子结点连接,达到任意结点的序列
。(如F到L的路径为:F-B-A-C-G-L)
子孙 :以某结点为根的子树中任意结点都称为该结点的子孙
。
森林 :由m(m>0)棵互不相交的树的集合称为森林
。
3.树的表示方法
树的表示方法有很多种:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法 。这里我们就了解最常用的孩子兄弟表示法。
struct TreeNode
{
struct TreeNode* child;//左边开始的第一个孩子节点
struct TreeNode* brother;//指向其右边的下一个兄弟节点
int data;//结点中的数据域
};
4. 树形结构实际运用场景
文件系统是计算机存储和管理文件的一种方式,它利用树形结构来组织和管理文件和文件夹 。在文件系统中,树结构被广泛应用
,它通过父节点和子节点之间的关系来表示不同层级的文件和文件夹之间的关联。
5.二叉树的概念与结构
在树形结构中,最常用的就是二叉树,一棵二叉树是结点的一个有限集合,该集合由一个根节点加上两棵别称为左子树和右子树的二叉树组成或者为空。
5.1二叉树的特点
-
二叉树中不存在度大于2的结点
。 -
二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
。
5.2 满二叉树
一个二叉树,如果每一层结点数都达到最大值,则这个二叉树就是满二叉树
。也就是说,如果一个二叉树的层数为k,且节点总数为2^k-1,则这棵二叉树就是一棵满二叉树。
5.3 完全二叉树
完全二叉树是由满二叉树引出来的,对于深度为k,n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号从1至n的结点一 一对应时称之为完全二叉树 。满二叉树是一种特殊的完全二叉树
。
5.4 二叉树的性质
根据满二叉树的特点可知:
1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)
个节点
2.若规定根节点的层数为1,则深度为h的二叉树的最大结点数为2^(h-1)
3.若规定根节点的层数为1,具有N个结点的满二叉树的深度
h=log(n+1)。(以2为底,n+1为对数)
5.5 二叉树的存储结构
二叉树一般有两种存储结构,顺序结构和链式结构。
5.5.1 顺序结构
顺序结构存储就是使用数组来存储 ,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费
。
看到这里,不知有没有小伙伴有一些疑问呢。使用数组存储,为什么二叉树的节点为空的的时候,还要为其保留一份空间呢?如果不保留的话,不就没有空间浪费了吗 ?这个问题问的非常好。那我们反向思考一下,这样做有什么用处呢?除根节点外,每个节点都有其父节点和子节点,那么我们使用数组来存储这些数据,怎么去确定其父节点和子结点的位置呢?我们能想到的只有数组下标了。现在我们再来看看刚才的问题,如果节点为空,在数组中我们不为其保留一份空间,那么我们还能表示出二叉树节点之中的关系吗
?答案当然是不能。
5.5.2 链式结构
二叉树的链式存储结构是指用链表来表示一棵二叉树 ,即用链来指示元素之间的逻辑关系。通常是链表中每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左右孩子所在的链节点的存储地址。
6. 堆的概念
如果有一个关键码集合K={K0,K1,K2,...Kn-1},把它的所有元素按完全
二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K(2*i+1)
(K(2*i+1)<=Ki<=K(2*i+2),i=0,1,2,...,则成为小堆(或大堆)。将
根节点最大的堆叫做大根堆(最大堆),根节点最小的堆叫做小根堆(或最小堆)。
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
6.1 堆的性质
.
堆中某个节点的值总是不大于或不小于其父节点的值
.
堆总是一棵完全二叉树
对于具有n个节点的完全二叉树,按照从上到下,从左至右的数组顺
序对所有节点从0开始进行编号,则对于序号为i的节点有:
1.若i>0,i位置节点的双亲序号:(i-1)/2;i=0,根节点,无双亲节点
2.若2*i+1<n,左孩子序号:2*i+1,否则,无左孩子
3.若2*i+2<n,右孩子序号:2*i+2,否则,无右孩子
6.2 堆的实现
6.2.1 堆的定义
c
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int capacity;//堆的容量
int size;//堆中有效数据的个数
}Heap;
6.2.2 堆的接口
c
//堆的初始化
void HeapInit(Heap* php);
//堆的销毁
void HeapDestroy(Heap* php);
//堆中插入数据
void HeapPush(Heap* php, HPDataType x);
//堆是否为空
bool HeapEmpty(Heap* php);
//删除堆顶数据
void HeapPop(Heap* php);
//获取堆顶数据
HPDataType HeapTop(Heap* php);
//堆的大小
int HeapSize(Heap* php);
6.2.3 堆的初始化
c
//堆的初始化
void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
6.2.4 堆中插入数据
c
//堆的插入
void HeapPush(Heap* php, HPDataType x)
{
assert(php);
//增容
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("HeapPush():realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
//插入数据
php->a[php->size] = x;
//先建堆再更新php->size的原因是需要使用子节点的下标求父节点的下标及比较子节点与父节点的大小,从而达到建堆的目的
//满足堆的特性
AdjustUp(php->a, php->size);
//更新堆中有效数据的个数
php->size++;
}
每一次插入数据,都需要检查容量是否足够,不够就需要扩容。第一次容量为0,给4个空间,否则就开辟2倍的空间,再插入数据,接下来就要进行建堆(小堆或大堆),最后,size自增。
6.2.5 向上调整算法
c
void Swap(HPDataType* x, HPDataType* y)
{
HPDataType tmp = *x;
*x = *y;
*y = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
//父节点的下标
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
// > 大堆
// < 小堆
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
当child的值大于parent的值时,就交换child和parent的值,让child走到parent的位置,再更新parent的值。child=0就不用在进行调整了,此时已经是一个有效的堆结构了。如果不符合这个条件,就可以直接退出循环。
6.2.6 堆是否为空
c
//堆是否为空
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
}
size记录堆中数据的个数,当size为0时,说明堆为空。
6.2.7 删除堆顶数据
c
//删除堆顶数据
void HeapPop(Heap* php)
{
assert(php);
//堆不能为空
assert(!HeapEmpty(php));
//根节点与最后一个节点交换
Swap(&php->a[0], &php->a[php->size - 1]);
//删除数据
php->size--;
//恢复堆结构
AdjustDown(php->a, 0, php->size);
}
首先,堆不为空才能删除数据。让堆顶数据与堆中最后一个数据进行交换,再让堆中数据个数size-1,最后调整堆结构,使之成为一个有效的堆。
6.2.8 向下调整算法(前提:左右子树必须是一个堆)
c
//恢复堆结构
void AdjustDown(HPDataType* a, int parent, int num)
{
//左孩子的下标
int child = 2 * parent + 1;
while (child < num)
{
//左右孩子比较大小
if (child + 1 < num && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[parent], &a[child]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
让最大的孩子节点与父节点进行交换,parent走到child的位置,更新child的位置。当child大于堆中数据的个数时,不再进行调整,如果已经是一个有效的堆,那么就跳出循环。
6.2.9 获取堆顶数据
c
//获取堆顶数据
HPDataType HeapTop(Heap* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
6.2.10 堆的大小
c
//堆的大小
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
6.2.11 堆的销毁
c
//堆的销毁
void HeapDestroy(Heap* php)
{
assert(php);
if (php->a != NULL)
{
free(php->a);
php->a = NULL;
}
php->capacity = php->size = 0;
}
a所指向的这块空间是动态开辟出来的,要还给操作系统,之后将size和capacity置为0。
7. TOP-K问题
TOP-K问题 :即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量比较大。
对于这种问题,我们能想到的直接方式就是对数据进行排序 ,但是如果数据量非常大的话,这种方式就不建议采取了,因为这么多的数据都不一定能够全部加载到内存中 。最好的方式就是用堆来解决
。
那我们应该想一想,我们应该建大堆还是建小堆呢?分情况讨论,如果要找前K个最大的数据,我们可以建小堆。让前K个数据入堆,后N-K个数据依次与堆顶数据进行比较,比堆顶数据大,就入堆。同理,找前K个最小的数据,我们应该建大堆,后N-K个数据依次与堆顶数据进行比较,比堆顶数据小的入堆。
总结:
.
找前K个最大的数据,建小堆
.
找前K个最小的数据,建大堆
举例:假如有10亿个数据,那么我们就需要多大的空间呢?我们可以简单的计算一下。
//10亿个字节
1GB=1024MB=1024*1024KB=1024*1024*1024Byte
//一个整型4个字节,10亿个整数就需要40亿个字节,也就是说我们需
要一下子开辟4GB的空间,这显然是不行的。
代码实现:
c
#include<iostream>
//创建N个数据到文件中
void CreateData()
{
int N = 100000;
srand((unsigned int)time(NULL));
FILE* file = fopen("data.txt", "w");
if (file == NULL)
{
perror("fopen fail");
exit(-1);
}
int randnum = 0;
for (int i = 0; i < N; ++i)
{
randnum = rand() % 100000 + 1;
fprintf(file, "%d\n", randnum);
}
fclose(file);
file = NULL;
}
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//恢复堆结构
void AdjustDown(int* minHeap, int parent, int num)
{
//左孩子的下标
int child = 2 * parent + 1;
while (child < num)
{
//左右孩子比较大小,建小堆
if (child + 1 < num && minHeap[child] > minHeap[child + 1])
{
child++;
}
if (minHeap[child] < minHeap[parent])
{
Swap(&minHeap[parent], &minHeap[child]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void TopK()
{
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
exit(1);
}
int k = 0;
printf("请输入:");
scanf("%d", &k);
//开辟k个数据的存储空间
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail");
return;
}
//前k个数据入堆
for (int i = 0; i < k; ++i)
{
fscanf(fout, "%d", &minHeap[i]);
}
//找前k个大数据,建小堆
//找前k个小数据,建大堆
for (int parent = (k - 1 - 1) / 2; parent >= 0; parent--)
{
AdjustDown(minHeap, parent, k);
}
//N-K个数据中找比堆顶数据大的入堆
int x = 0;
while (fscanf(fout, "%d", &x)!=EOF)
{
if (x > minHeap[0])
{
minHeap[0] = x;
AdjustDown(minHeap, 0, k);
}
}
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
fout = NULL;
free(minHeap);
minHeap = NULL;
}
int main()
{
//CreateData();
TopK();
return 0;
}