树的概念及结构
1.树的概念
树是一种非线性的数据结构,它是由n个有限结点组成的一个具有层次关系的集合。把他叫做树是因为看起来像一个倒挂的树,也就是说它根朝上,叶朝下。
- 有一个特殊的结点,称为根结点,根结点没有前驱点。
- 除根结点外,其余结点被分成M(M>0)个互不相交的集合T1,T2……,Tm,其中每一个集合Ti(1<=i<=m)又是一个结构与树类似的结构。每棵树的根结点有且只有一个前去,可以有0个挥着多个后继。
- 树是递归定义的。
注:树型结构中,子树之间不能有交集。
树与非树?
- 子树是不相交的。
- 除了根结点,每个结点有且只有一个父节点。
- 一颗N个结点的树有N-1条边。
2。树的相关概念
结点的度:一个节点含有子树的个数称为该结点的度;如:A 的度为6.
叶节点或终端节点:度为0的节点称为叶节点;如:B
非终端结点或分支节点:度部位0的结点;如:D
双亲结点或父节点:若一个结点含有子节点,则这个节点称为其子节点的父节点;如:A是B的父节点。
孩子节点或子节点:一个结点含有的子树的根结点称为该节点的子节点;如:B是A的函子结点。
兄弟节点:具有相同的父结节点称为兄弟节点;如上图:B,C是兄弟节点。
树的度:一颗树中,最大使得节点称为树的度;如:上图树的度为6。
节点的层次:从根节点定义起,根为第一层,根的子节点为第二层,以此类推。
堂兄弟节点:双亲在同一层的节点互为堂兄弟。如:H,I互为堂兄弟。
节点的祖先:从根到该节点所经分支上的所有节点。如:A是所有节点的祖先。
子孙:以某节点为跟的子树中任意节点都称为该节点的子孙,如:所有的节点都是A的子孙。
森林:有m(m>0)棵互不相交的属的集合称为森林。
3.树的表示
树结构相对于线性表上升了一个难度,储存起来比较麻烦。既然保存值域,也要保存节点和节点之间的关系,实际上树的表示方法有多种:双亲表示法,孩子表示法,孩子双亲表示法,孩子兄弟表示法。
最常用的是孩子兄弟表示法。
画图表示:
4.树在实际中的应用
文件夹和子文件夹增删查改。
二.二叉树概念及结构
1.概念
一颗二叉树是节点的一个有限集合,
该集合为空或者由一个根结点加上两颗别称为左子树和右子树组成。
从上图可得出:
1.二叉树不存在度大于2的节点。
2.二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
2.特殊的二叉树
1.满二叉树:一个二叉树,每层的节点数都达到最大值。
2.完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是满二叉树印出来的。对于深度为k的,有n个节点的二叉树,当且仅当每一个节点斗鱼深度为k的满二叉树中编号从1~n的节点—对应时曾为完全二叉树。且满二叉树是一种特殊的完全二叉树。
3.二叉树的性质
1.规定根结点的层数为1,则一颗非空二叉树第i层最多有2^(i-1)个。
2.规定根节点的层数为1,则深度为h的二叉树的最大节点数为2^h−1.
3.任何一个二叉树,如果度为0其叶节点的个数时𝑛0,度为2的节点个数是𝑛2,则有𝑛0 = 𝑛2+1.证明:数几次就有了。
4.规定根结点的层数为1,具有n个节点的满二叉树的深度,h = log2𝑁+1
5.对于具有N个节点的完全二叉树,如果按照从上至下从左至右的数组顺序从0开始编号,则对与序号为i的节点有:
1.若i>0,i位置节点的双亲序号为:(i-1)/2,i为根结点编号,无双亲结点。
2.若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子。
3.若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子。
小试牛刀:
1.某二叉树共有399个结点,其中有199个度为2的结点,则改二叉树的叶子节点数为:200
根据公式:𝑛0 = 𝑛2+1.
2.在具有2n个节点的完全二叉树中,叶子节点的个数为:
A n B n+1 | C n-1 | D n/2 |
度为0的结点 𝑛0,度为2的结点𝑛2,度为1的结点1或0
根据公式:𝑛0 = 𝑛2+1
可得:2 𝑛0 -1 +1/0 = 2n
化简: 为n 或(2𝑛0 -1)/2 -- 非整数舍去
3.一个完全二叉树的结点位数为531个,那么这棵树的高度为(B)
A.11 | B.10 | C.8 | D.12 |
夹逼法,计算两个极限情况
1.以满二叉树结束:2^h−1 = 531 h>9
2.以完全二叉树结束,最后一层只有一个结点:2^(h−1)=531 11 > h >10
4.二叉树的储存结构
二叉树可以使用两种结构储存,一种是顺序结构,另一种是链式结构。
1.顺序结构
顺序结构储存使用数组来储存,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。现实中只有对才会使用数组来储存,二叉树顺序储存在物理上是一个数组,在逻辑上是一个二叉树。
2.链式储存
二叉树的链式储存结构是指:用链表来表示一棵二叉树,即用链来指数元素之间的逻辑关系。通常的方法是i岸标中每个结点由3个域组成,数据与和左右指针域,左右指针分别用来给出该节点左孩子和右孩子所在的连接点的储存地址。链式结构又分为二叉链和三叉链。
三:二叉树的顺序结构及实现
1.二叉树的顺序结构
见上,本质上是数组
2.堆的概念及结构
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二树的顺序存储方式存储 在一个一维数组中,并满足: <= 且 <=( >= 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质
- 堆中的某个节点的值总是不大于或不小于其父节点的值
- 堆宗室一颗完全二叉树。
3.堆的实现
以数组的形式创建堆。
创建结构体:
//创建小堆
typedef int HPDataType;
//以数组的形式实现完全二叉树
typedef struct Heap
{
HPDataType* a;//创建指针数组
size_t capacity;//容量
size_t size;//大小
}HP;
1.初始化
和对顺序表初始化相同。
//初始化
void HPInit(HP* php)
{
//断言,防止传递的指针为野指针
assert(php);
//初始化
php->a = NULL;
php->capacity = php->size = 0;
}
2.销毁堆
与顺序表销毁流程不能说是毫不相关只能说一模一样。
//销毁
void HPDestroy(HP* php)
{
//断言
assert(php);
//释放申请的内存
free(php->a);
php->a = NULL;
//置空
php->capacity = php->size = 0;
}
3.插入数据
核心:在插入数据的同时,不能破环其堆结构,依旧得是小/大堆。
思路:判断是否需要扩容,判断条件:size == capacity。插入数据。
以上与顺序表完全相同。
向上调整堆,保证其还是一个小堆。
AdjustUp(php->a, php->size - 1);
向上调整:先将数据插入到堆中,向上调整。找到子节点和这个子节点的父亲,比较大小。交换父亲和孩子结点的值,循环下去,child位于祖先结点时,循环结束。
画图:
现在插入3
插入后不再是一个完整的小堆,需要向上调整。
更新父亲和孩子:child = parent
parent = (child-1)/2
已经是小堆了,跳出循环。
极限情况:插入的数是最小的,那么它就会移至根结点,此时child = 0。
代码:
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustUp(HPDataType* a, size_t child)
{
//找到子节点,以及这个孩子的父亲
size_t parent = (child - 1) / 2;
//比较父亲和孩子的大小
while (child > 0)
{
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
}
else
{
break;
}
child = parent;
parent = (child - 1) / 2;
}
}
//插入数据,保证堆还是小/大堆
void HPPush(HP* php, HPDataType x)
{
//断言
assert(php);
//判断是否要扩容
if (php->capacity == php->size)
{
size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
//realloc动态开辟空间
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
php->a = tmp;
php->capacity = newcapacity;
}
}
//插入数据
php->a[php->size] = x;
php->size++;
//向上调整堆,保证其还是一个小堆
AdjustUp(php->a, php->size - 1);
}
打印“堆”
本质上是数组,遍历打印就可以了。
void HPPrint(HP* php)
{
for (size_t i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
删除堆顶的数据
第一时间考虑的是:将数组中的数据向前覆盖掉头的位置,但是且不说删头的时间复杂度为O(N),之后还无法保证依旧为大/小堆。
就拿上面提及的那个堆:
删头后:
已经不是完整的小堆或者大堆。
最优处理情况:将头和尾的数据交换,--size,之后再进行向下调整即可。
向下调整:找到子树中较小的一个支,交换数据,以此循环。
坑点:1)完全二叉树其不一定具有右子树。
2)需要更新父和子结点。
画图:
对这棵树进行处理
找到父子结点,交换位置。
更新父子结点。
循环,直至称为标准的小堆。
代码:
AdjustDown(HPDataType* a, size_t size, size_t root)
{
size_t parent = root;
size_t child = parent * 2 + 1;
//
while (child < size)
{
//找出孩子中较小的那一个,注意完全二叉树可能不存在右孩子
//确定左右子树谁的值更小
if (a[child] > a[child + 1] && child + 1 < size)
{
//默认是左<右,这里是判断调整
child++;
}
//如果孩子小于父亲就交换
if (a[child] < a[parent])
{
Swap(&a[parent], &a[child]);
}
else
{
break;
}
//继续向下调整
parent = child;
child = parent * 2 + 1;
}
}
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);
}
判断堆是否为空
思路:秩序返回size==0的判断值即可,原因:size指向的是数组的下一位。
bool HPEmpty(HP* php)
{
assert(php);
//用表达式的返回值来判断,为0就为空
return php->size == 0;
}
返回栈顶的值
思路:保证堆至少不为空,返回数组的首元素的值。
size_t HPSize(HP* php)
{
assert(php);
//根据数组的性质,可直接返回size的值
return php->size;
}
堆的大小
思路:数组的特性,size指向的是下一位,但是其正好是堆的大小。
HPDataType HPTop(HP* php)
{
assert(php);
//堆不为空
assert(php->size > 0);
return php->a[0];
}
根据模拟实现的堆,模拟实现堆排序。
void HeapSort(int* a,int size)
{
HP hp;//创建堆
HPInit(&hp);//初始化
for (int i = 0; i < size; i++)
{
HPPush(&hp, a[i]);//插入数据并构建小堆
}
while (!HPEmpty(&hp))//堆不为空,就继续
{
printf("%d ",HPTop(&hp));//取出堆顶的数据
HPPop(&hp);//删去对顶的数据
}
printf("\n");
HPDestroy(&hp);//销毁堆
}
int main()
{
int a[] = { 1,5,6,9,8,2,3,7,4 };
int size = sizeof(a) / sizeof(a[0]);
HeapSort(a,size);
return 0;
}
看其空间复杂度为O(N),时间复杂度为O(NlogN).
能不能优化???
优化方案尽情期待。