二叉树的介绍及其顺序结构的实现

近听水无声477 2024-08-28 11:35:01 阅读 56

Hello, 亲爱的小伙伴们,你们的作者菌又回来了,之前我们学习了链表、顺序表、栈等常见的数据结构,今天我们将紧跟之前的脚步,继续学习二叉树。

好,咱们废话不多说,开始我们今天的正题。

1.树

1.1树的概念和结构

树是一种非线性的数据结构,他是由n(n >0)个有限的节点组成的一个具有层次结构的集合。

树是因为他的外形看起来像是一颗倒挂的树,也就是根朝上,叶子朝下。

有一个特殊的节点———根节点,根节点没有前驱节点。

除根节点外,其余的节点都被分成了M(M>0)个互不相交的集合,其中每一个集又是和树结构相似的子树,每颗子树的根节点有且只有一颗根节点,可以有无数个子节点,因此树是由递归定义的!

在树型结构中,子树不能有交集,否则就不是树的结构。

比如以下的非树形结构:

 子树是不相交的。

除了根节点外,每个节点有且只有一个父节点。

一颗有N个节点的树有N-1个边。

1.2树的相关术语

 父节点/双亲结点:若有一个节点含有子节点,则这个节点成为子节点的父节点;如上图中的B的父节点为A.

子节点/孩子节点:一个节点含有的子树的根节点,如上图中·:B是A的孩子节点。

节点的度:一个节点有几个孩子节点,他的度就是多少,比如:A的度是2;F的度是2

树的度:树的度指的就是最大节点的度。

分支节点:度不为0的节点。

兄弟节点:具有相同父节点的节点成为兄弟节点,比如E、F节点就是兄弟节点。

节点的层次:从跟开始定义起,跟为第1层,根节点的子节点为第2层,以此类推。

树的高度和深度:树中节点的最大层次。比如上面的树的最大层次就是4,所以这棵树的深度就是4.

子孙:以某节点为根的子树中任意节点都可以称为该节点的子孙。

森林:多颗不相干的树的集合就是森林。

1.3树的表示

孩子兄弟表示法:

树的结构相较于线性表就要复杂的多,要存储起来也比较麻烦,既要保存值又要保存节点与节点之间的关系,实际上树的表示方式有很多种,比如:双亲表示法、孩子表示法、孩子双亲表示法、以及孩子兄弟表示法等,这里我们就简单的看看孩子兄弟表示法:

<code>struct TreeNode

{

struct Node* child; // 左边开始的第⼀个孩⼦结点

struct Node* brother; // 指向其右边的下⼀个兄弟结点

int data; // 结点中的数据域

}

1.4树形结构的应用场景 

文件系统是计算机存储和文件管理的一种方式,他利用树形结构来组织和管理文件和文件夹,在文文件夹中,树的结构被广泛的应用,他通过父节点和子节点之间的关系来展示不同层级的文件和文件夹之间的关系。

 2.二叉树

2.1概念与结构

在树的结构中。我们常见的就是二叉树,一颗二叉树的节点的设计一个有限的集合,该集合是由根节点加上两颗别称为左子树和右子树的集合所构成的。

从上面的图我们能得出一些相关的信息:

1.二叉树不存在度大于2的节点

2.二叉树的子树又左右之分,次序不能颠倒,因此二叉树是有序树。

注意:对于任何的二叉树都是由以下的情况复合而成的:

 

 2.2特殊的二叉树

一颗二叉树,如果每一层的节点数都达到了最大值,则这棵树就是满二叉树。也就是说,如果这颗树的层数是k。且节点的总数是2^k - 1,则他就是满二叉树。

完全二叉树是效率非常高的数据结构,完全二叉树是由满二叉树的概念引述而来的。对于深度为k的,有n个节点的二叉树吗,当且仅当其每一个节点都与深度为k的满二叉树中的编号从1到n的节点一一对应就可称其为二叉树,要注意:满二叉树是一种特殊的完全二叉树.

2.3二叉树的存储结构

二叉树一般可以使用两种储存结构,一种是顺序结构,一种是链式结构 

2.3.1顺序结构

顺序结构存储就是使用数组作为底层结构,一般数组适用于表示完全二叉树,因为不是完全二叉树就有空间的浪费,完全二叉树更适合使用顺序结构存储。

 3.实现顺序结构二叉树

一般对堆使用使用顺序结构的数组来存储数组,堆是一种特殊的二叉树,具有二叉树的特性同时,还具备其他的特性。

3.1堆的结构和概念

如果有一个关键码的集合K = {

k_{0},k_{1},k_{2},k_{3},.....k_{n -1}

},他把所有的元素按照完全二叉树的存储结构存储起来,在一个一维数组中,并满足

^{_{}}

k_{i} <= k_{2*i + 1}

k_{i}>= k_{2*i + 1}

k_{i}<= k_{2*i+2}

)i = 0、1、2、3.....,则称之为小堆(或大堆)。将节点最大的堆叫做最大堆或者是大根堆,根节点最小的堆叫做小堆和小根堆。

堆具有以下的性质:

堆中某个节点的值总是不大于或不小于其父节点的值。

堆是一颗完全二叉树 

• 对于具有 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堆的实现

首先我们还是要先创建三个文件,来实现堆:

3.2.1堆的定义

底层的结构·是数组,所以对结构的定义就和顺序表的定义差不多

<code>typedef int HpDataType;

typedef struct Heap

{

HpDataType *arr;

int size;//记录有效元素的个数

int capacity;//记录申请的空间容量

}Hp;

3.2.2堆的初始化(HpInit)和销毁(HpDestroy)

由于底层结构就是由数组构成的,所以在初始化上,顺序表和堆十分的相似:

1.HpInit

定义:

//堆的初始化

void HpInit(Hp* php);

实现:

/堆的初始化

void HpInit(Hp* php)

{

assert(php);

php->capacity = php->size = 0;

php->arr = NULL;

}

2.HpDstroy

定义:

//堆的销毁

void HpDstroy(Hp* php);

实现:

void HpDstroy(Hp* php)

{

assert(php);

free(php->arr);

php->arr = NULL;

php->capacity = php->size = 0;

}

由于底层结构的相似性,我们在实现堆的从初始化和销毁时,就和顺序表的初始化和销毁相似。

测试:

初始化时,s.arr = NULL; s.capacity = s.size = 0;

3.2.3 堆数据的插入(HpPush)(入堆)

堆数据的插入和顺序表的数据插入也有相似之处:

定义:

<code>//堆的数据插入

void HpPush(Hp* php, HpDataType x);

实现:

void HpPush(Hp* php, HpDataType x)

{

assert(php);

//判断空间是否足够

if (php->capacity == php->size)

{

//扩容

int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;

HpDataType* ptmp = (HpDataType*)realloc(php->arr, sizeof(HpDataType) * newcapacity);

if (ptmp == NULL)

{

perror("realloc Fail!!");

exit(1);

}

php->arr = ptmp;

php->capacity = newcapacity;

}

php->arr[php->size++] = x;

AdjustUp(php->arr, php-> size - 1);//向上调整算法

}

和顺序表的数据插入相似,我们都要先确保空间的充足,才能进行插入操作!!

重要的是,我们需要理解向上调整算法的含义:

3.2.3.1向上调整算法(AdjustUp)

向上调整算法:将新的数据插入到数组的尾部,在进行向上的调整,直到满足堆的结构!!

先将元素插到堆的末尾,即最后一个孩子之后

插入之后如果堆的性质遭到破坏,将新插入的节点顺着其双亲往上调整到合适得位置就好额!!

 代码实现:

<code>void Swap(int* x, int* y)

{

int tmp = *x;

*x = *y;

*y = tmp;

}

void AdjustUp(HpDataType* arr, int child)

{

int parent = (child - 1) / 2;

while (child > 0)//当child节点为0是,该节点已经到达这颗树的顶端,无需再调整!!

{

if (arr[child] > arr[parent])//这里采用 < 最后就能建成小堆;采用>就能建成大堆

{

Swap(&arr[child], &arr[parent]);

child = parent;

parent = (child - 1 )/ 2;

}

else

{

break;

}

}

}

代码测试:

我们建成的是大堆吗?

我们画图来验证一下:

 

 从这里我们就能看出,建成大堆成功!!

3.2.4堆中数据的删除(HpPop)(出堆)

注意:出堆,与出队列的定义相似,出堆是将堆头的数据删去!!

定义:

<code>//堆中数据的删除!!

void HpPop(Hp* php);

实现:

我们可以怎样来删去堆头的数据呢?

也许,我们可以让堆头的数据与堆尾的数据相交换,删除堆尾的数据容易,且不会改变原来的数据结构。

所以我们又会涉及到一种在堆中的向下调整算法:

3.2.4.1向下调整算法(AdjustDown)

注意:向下调整算法有一个前提:左右子树必须为一个堆!! 

1.将堆顶元素与最后一个元素交换 

2.删除堆中的最后一个元素

3.将堆顶的元素向下调整直到满足堆的特性为止!

 

 代码实现:

<code>void AdjustDown(int* arr, int parent, int n)

{

int child = parent * 2 + 1;

if (arr[child] < arr[child + 1])

{

child++;

}

while (child < n)//注意:child最大时为 n-1,注意不能越界!!

{

if (arr[child] > arr[parent])

{

Swap(&arr[parent], &arr[child]);

parent = child;

child = 2 * parent + 1;

}

else

break;

}

}

所以完整的代码我们就可以写成:

void AdjustDown(int* arr, int parent, int n)

{

int child = parent * 2 + 1;

if (arr[child] < arr[child + 1])//找出左右孩子节点中数值较大那个

{

child++;

}

while (child < n)

{

if (arr[child] > arr[parent])//如果孩子节点的值比父节点大,就进行交换

{

Swap(&arr[parent], &arr[child]);

parent = child;

child = 2 * parent + 1;

}

else

break;

}

}

//堆中数据的删除!!

void HpPop(Hp* php)

{

assert(php && php->size);

//最后一个元素与第一个元素相交换

Swap(&php->arr[0], &php->arr[php->size - 1]);

php->size--;

//开始向下调整

AdjustDown(php->arr, 0, php->size);

}

代码测试:

我们来比对一下删除元素前后的效果:

 

 可以看到交换后依然符合大堆的性质!!!

3.2.5取堆顶元素和堆的判空操作(HpTop && HpIsEmty)

在实现堆的部分性质时,我们很容易就会联想到,我们曾经学习过的数据结构,如:栈、队列和顺序表单链表等。

这里的取堆顶元素就和栈的性质相似,堆的数据也不能进行随机访问,所以,想要进行堆数据的打印,就要进行数据的以此访问和打印,所以我们需要结合判空、取堆顶元素和去堆顶元素来实现堆数据的打印。

判空和取堆顶数据的定义:

 

<code>//取堆顶元素

HpDataType HpTop(Hp* php);

//判空

bool HpIsEmpty(Hp* php);

这里的两个函数实现和之前的操作相似,我们这里就不多叙述,代码也十分的简单,感兴趣的朋友也可以去看看我以前的文章:

 

HpDataType HpTop(Hp* php)

{

assert(php && php->size);

return php->arr[0];

}

bool HpIsEmpty(Hp* php)

{

assert(php);

return php->size == 0;

}

所以最后我们就可以将堆中的数据打印出来:

这样我们就实现了堆数据的打印了!! 

结语

二叉树的顺序结构我们就学习到这里,感谢大家的阅读,如果你有不懂得问题也欢迎大家来评论区理性讨论,咱们下期再见,拜拜!!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。