1.树的概念与结构
树是一种非线性的数据结构,它n(N>=0)个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂着的树,也就是说它是根朝上,而叶朝下的。
- 有一个特殊的节点,称为根节点,根节点没有前驱节点。
- 除根节点外,其余节点被分成M(M>0)个互不交的集合,其中每一个集合又是一棵结构与树类似的子树。每棵子树的根节点有且仅有一个前驱,可以有0个或多个后驱。因此树是递归定义的。
2.树的相关术语
父亲节点/双亲节点:若有一个节点含有子节点,则这个节点称为其子节点的父节点。
子节点/孩子节点:一个节点含有的子树的根节点称为该节点的子节点。
节点的度:一个节点有几个孩子,它的度就是多少。
树的度:一棵树中,最大的节点的度称为该树的度。
叶子结点/终端节点:度为0的节点称为叶子结点。
分支节点/非终端节点:度不为0的节点。
兄弟节点:具有相同的父节点互称为兄弟节点(亲兄弟)。
节点的层次:从根开始定义起,根为第一层,根的子节点为第二层。
树的高度或深度:树中节点的最大层次。
节点的祖先:从根到该节点所经分支上的所有的节点
路径:一条从树中任意节点出发,沿父子节点-子节点连接,达到任意节点的序列。
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林。
3.二叉树的概念与结构
在树形结构中,我们最常用的就是二叉树,一棵二叉树就是节点的一个有限集合,该集合是由一个根节点加上两棵分别为左子树和右子树的二叉树组成或者为空。
二叉树具有的特点:
- 二叉树不存在度大于2的节点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
注意:对于任意的二叉树都是由以下几种情况复合而成的,空树(度为0),只有根节点(的二叉树),只有左子树(度为1),只有右节点(度为1),左右子树均存在(度为2)。
4.特殊的二叉树
4.1.满二叉树 :
一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。
也就是说,如果一个二叉树的层数为k ,且节点的总数是2^k-1,则它就是满二叉树。 (会涉及到等比数列求和)
4.2.完全二叉树:
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。
对于深度为k的,有n个节点的二叉树,当且仅当其每一个节点都与深度为k 的满二叉树的满二叉树中编号从1至n的节点----对应时称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树。(假设二叉树层次为k,除了第k 层外,每层节点的个数的达到最大节点数,但第k层节点个数不一定达到最大节点数。 )
注:完全二叉树的节点的顺序是从左到右的。
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
5.二叉树的存储
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
5.1顺序存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树就会有空间的浪费,完全二叉树更适合使用顺序结构存储。
5.2 链式存储就是使用链表俩表示一棵二叉树,即用链表来指示元素的逻辑关系。通常的方法是链表中每个节点由三个域组成,数据域和左右指针域,左右指针域分别用来给出该节点做孩子和右孩子所在的链表节点的存储地址。链式结构又分为二叉连和三叉链,当前我们学习中一般使用的都是二叉连。后面学习到高阶数据结构如红黑树会用到三叉链。
6.实现顺序结构二叉树(小堆)
一般使用顺序结构来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他特性。
6.1 堆的概念与结构
如果有一个关键码的集合k,把它所有的元素按完全二叉树的顺序存储方式,在一个一维数组中,并满足K(i)<=K(2*i+1)或者K(i)>K(2*i+1)且K(i)<=K(2*i+2),则称为小堆(大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
注意:
现实中我们通常把堆(一种二叉树)使用顺序结构来存储,需要注意的是这里的堆和操作系统的虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统管理内存的一块区域分段。
6.2 堆具有以下性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值。
- 堆总是一棵完全二叉树。
补充
- 小堆的堆顶是堆的最小值;大堆的堆顶是堆的最大值。
6.3 二叉树的性质:
对于具有n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号i的节点有:
- 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点。
- 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子;
- 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子。
6.4 堆的实现
和前面的数据结构实现相类似,都需要三个文件Heap.h(堆结构的定义以及函数的声明),Heap.c(函数功能的实现),test.c(函数功能的测试)。
6.4.1 Heap.h(堆结构的定义以及函数的声明)
堆底层结构为数组,因此定义堆的结构和顺序表的结构类似。
#include <stdio.h>
#include <stdlib.h>
#include <assert.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 HPPush(HP* php, HPDatatype x);
//判空
bool HPEmpty(HP* php);
//删除数据
void HPPop(HP* php);
//获取堆顶数据
HPDatatype HPTop(HP* php);
//获取堆底的最后一个数据
HPDatatype HPBottom(HP* php);
//获取堆中有效的数据个数
int HPSize(HP* php);
6.4.2 Heap.c(函数功能的实现)
#include "Heap.h"
6.4.2.1 堆的初始化和销毁
//初始化
void HPInit(HP* php)
{
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
//销毁
void HPDestroy(HP* php)
{
assert(php);
if (php->arr)
free(php->arr);
php->arr = NULL;
php->size = php->capacity = 0;
}
【实现思路】
因为堆底层结构为数组,因此定义堆的结构和顺序表的结构类似。所以堆的初始化和销毁的实现和顺序表的销毁和初始化基本相同。
6.4.2.2 堆的插入数据(基于小堆)
//进行数据交换
void Swap(HPDatatype* x, HPDatatype* y)
{
HPDatatype temp = *x;
*x = *y;
*y = temp;
}
//进行向上调整
void AdjustUp(HPDatatype* arr, int child)
{
assert(arr);
//由孩子节点求出双亲节点 进行大小比较
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;
}
}
}
//插入数据 --建立小堆
void HPPush(HP* php, HPDatatype x)
{
assert(php);
//先判断空间够不够
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDatatype* temp = (HPDatatype*)realloc(php->arr,newcapacity*sizeof(HPDatatype));
if (temp == NULL)
{
perror("realloc fail");
exit(1);
}
php->capacity = newcapacity;
php->arr = temp;
}
php->arr[php->size]=x;
//入堆是入堆底,需要进行 先 向上调整 ,再进项数据个数的更新
AdjustUp(php->arr, php->size);
php->size++;
}
【实现思路】
先进行所传地址的有效性的判断,再进行判断空间够不够,(判断空间够不够和顺序表的基本相同 ),其次再进行数据的插入。不过只将数据插入后,整体的结构未必满足堆的概念,所以在这里我们需要对堆中的数据进行调整(向上调整法)。
在实现向上调整法的时候,我们要一直保持父节点的值始终小于或等于孩子节点,所以我们需要通过孩子节找父亲节点,找到父亲节点之后需要和当前的孩子节点进行比较,孩子小就进行向上调整(如果孩子大就不需要进行调整,就直接跳出循环),即就是就进行数据的交换。
每次调整完之后需要将指向当前孩子节点的变量向上移,当孩子节点指向根节点的时候终止循环。
调整完之后需要进行数据的++。
注意:向上调整方法的传参,需要传底层数组的地址和当前堆中有效的数据个数。
6.4.2.3 堆的判空
//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
【实现思路】
我们需要先对所传地址的有效性进行判断,然后直接将堆中的有效的数据个数是否为0返回。(函数的返回值类型为bool类型)。
6.4.2.4 堆的数据删除(基于小堆)
//向下调整方法 依照小根堆
void AdjustDown(HPDatatype* arr, int parent, int size)
{
//计算出的是左孩子
int child = 2 * parent + 1;
while (child < size)
{
//找左,右孩子中最小值
if (child+1<size && arr[child] > arr[child + 1])
child++;
if (arr[parent] > arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child= 2 * parent + 1;
}
else
{
break;
}
}
}
//删除数据 删除堆顶的数据
void HPPop(HP* php)
{
assert(php);
assert(php->size);
//先进行数据交换,再进行数据的调整
Swap(&php->arr[0], &php->arr[php->size - 1]);
//数据个数减1
php->size--;
//进行数据的调整
//向下调整法
AdjustDown(php->arr,0, php->size);
}
【实现思路】
先判断所传地址的有效性,再进行堆的判空,如果堆为空则不能删除数据,堆不为空就可以进行删除数据。
堆中要删除数据是删除的堆堆顶的元素。所以要先将堆顶的数据和堆底的最后一个元素先进行交换,再进行堆的size--,最后进行堆中数据的调整。(因为交换之后堆不一定能满足堆的结构,所以需要进行堆的调整)。
在这里我们删除要实现的堆的调整是向下调整。在实现堆的向下调整的方法的时候,要保持父节点的值小于或等于孩子节点的值。我们在实现该方法的时候我们需要通过父亲节点找孩子节点。 通过当前的父亲节点找到其孩子节点,然后比较大小,如果父亲节点的值比其孩子节点的值大就交换两者的所对应的值。(反之就不交换,直接跳出循环)每次这样调整完之后,需要将指向当前父亲节点的变量指向其孩子节点的位置,然后将孩子节点移动到下一层(即孩子的孩子节点)。
终止循环的条件为,当孩子节点刚走出最后一个位置时。
注意:向下调整方法的传参,堆的底层数组的地址,首元素的下标,堆中有效的元素的个数。
6.4.2.5 获取堆顶数据
//获取堆顶数据
HPDatatype HPTop(HP* php)
{
assert(php&&php->size);
return php->arr[0];
}
【实现思路】
先判断所传地址的有效性,然后将堆的底层数组的第一个数据的值返回即可。
6.4.2.6 获取堆底的最后一个数据
//获取堆底的最后一个数据
HPDatatype HPBottom(HP* php)
{
assert(php&&php->size);
return php->arr[php->size - 1];
}
【实现思路】
先判断所传地址的有效性,然后将堆的底层数组的最后一个数据的值返回即可。
6.4.2.7 获取当前堆的有效的数据个数
//获取堆中有效的数据个数
int HPSize(HP* php)
{
assert(php);
return php->size;
}
【实现思路】
先判断所传地址的有效性,然后将堆中的size值返回即可。
6.4.3 test.c(函数功能的测试)
#include "Heap.h"
//ܵIJ
void test()
{
HP hp;
HPInit(&hp);
int arr[]= { 17,20,10,13,19,15 };
for (int i = 0; i < 6; i++)
{
HPPush(&hp, arr[i]);
}
while (!HPEmpty(&hp))
{
printf("%d ", HPTop(&hp));
HPPop(&hp);
}
HPDestroy(&hp);
//剩余函数的功能测试可以自己进行测试
}
int main()
{
test();
return 0;
}