朋友们好,这里是针对二叉树中的概念,基本操作,存储结构,整理出来一篇博客供我们一起复习和学习,如果文章中有理解不当的地方,还希望朋友们在评论区指出,我们相互学习,共同进步!
文章目录
一:树的概念与结构
1.1 树的概念
树是一种非线性的数据结构,它是由n (n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
●有一个特殊的结点,称为根结点,根节点没有前驱结点
●除根节点外,其余结点被分成M(M>O)个互不相交的集合T1、T2、…Tm,其中每一个集合Ti(1<= i<
=m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
●因此,树是递归定义的。
注意:树形结构中,子树之间不可以有交集,否则就不是树形结构!
1.2 树的相关概念
- 节点的度:一个节点含有的子树的个数称为该节点的度;如上图:A的为6。
- 叶节点或终端节点:度为0的节点称为叶节点;如上图: B、C、H、…等节点为叶节点。
- 非终端节点或分支节点:度不为0的节点;如上图:D、E、F、G…等节点为分支节点。
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如上图:A是B的父节点。
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;如上图:B是A的孩子节点。
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;如上图:B、C是兄弟节点。
- 树的度:一棵树中,最大的节点的度称为树的度;如上图:树的度为6。
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次;如上图:树的高度为4。
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、l互为兄弟节点。
- 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先。
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。
- 森林:由m (m>O)棵互不相交的树的集合称为森林。
1.3 树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
代码示例:
typedef int DataType;
struct Node
{
struct Node * _firstChild1;//第一个孩子节点。
struct Node * _pNextBrother;//指向下一个兄弟节点。
DataType _Data; //节点中的数据欲
};
1.4 树在实际中的运用(表示文件系统的目录树结构)
引用自比特科技课件。
二:二叉树的概念与结构
2.1 概念
一棵二叉树是节点的一个有限集合,该集合:
- 由一个根节点加上两颗分别称为左子树和右子树的二叉树组成。
- 或者为空。
从上图可以看出: - 二叉树不存在度大于2的节点。
- 二叉树的子树有左右之分,次序不可以颠倒,因此二叉树是有序树。
2.2 特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是25一1,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。要注意的是满叉树是一种特殊的完全二叉树。
2.3 二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点。
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2h-1。
- 对任何一棵二叉树,如果度为0其叶结点个数为N0,度为2的分支结点个数为N2,则有N0 = N2 + 1。
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n + 1)。(ps: log2(n+1)是log以2为底,n+1为对数)
- 对于具有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.1 顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
引自比特科技课件
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2 堆的概念及结构
如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: Ki<= K(2 * i + 1)且Ki<= K(2 * i + 1) ,则称为小堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或者不小于其父节点的值。
- 堆总是一颗完全二叉树。
小根堆示例:
3.3 堆的实现
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
3.3.1 堆向下调整算法
代码示例:
void AdjustDown(HPDataType* a, size_t size, size_t root){
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
//选出左右孩子小的那一个
if (child + 1 < size && a[child + 1] < a[child])
{
child++;
}
//向下调整,如果孩子小于父亲,则交换,继续向下调整
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
3.3.2 堆向上调整算法
void AdjustUp(HPDataType* a, HPDataType child){
assert(a);
//int child = php->size - 1;
int parent = (child - 1) / 2;
while (a[parent] > a[child] && parent >= 0)
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (child - 1) / 2;
}
}
3.3.3 堆的代码实现
实现小顶堆:
// 小堆
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
size_t size;
size_t capacity;
}HP;
void Swap(HPDataType* pa, HPDataType* pb);
//堆的初始化
void HeapInit(HP* php);
//堆的销毁
//void HeapDestroy(HP* php);
//堆的打印
void HeapPrint(HP* php);
//堆的插入 插入x以后,保持他依旧是(大/小)堆
void HeapPush(HP* php, HPDataType x);
//堆的删除 删除堆顶的数据。(最小/最大)
void HeapPop(HP* php);
//堆的判空
bool HeapEmpty(HP* php);
//堆的数据个数
//size_t HeapSize(HP* php);
//取堆顶的数据
HPDataType HeapTop(HP* php);
A: 堆的插入
1. 先将元素插入到堆的末尾,即最后一个孩子之后。
2. 插入之后,如果堆的性质遭到破坏,将新插入节点顺着其双亲往上调整到合适的位置。
上图也即先插入10到数组的尾上,再进行向上调整算法,直到满足堆。
代码实现:
void AdjustUp(HPDataType* a, HPDataType child){
assert(a);
//int child = php->size - 1;
int parent = (child - 1) / 2;
while (a[parent] > a[child] && parent >= 0)
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (child - 1) / 2;
}
}
void HeapPush(HP* php, HPDataType x){
assert(php);
if (php->capacity == php->size){
size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType * temp = realloc(php->a, sizeof(HPDataType) * newcapacity);
if (!temp)
{
printf("realloc fail\n");
exit(-1);
}
else{
php->a = temp;
php->capacity = newcapacity;
}
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
B:堆的删除
删除堆是删除堆顶的数据,将堆顶的数据与最后一个数据交换,然后删除数组最后一个数据再进行向下调整算法。
步骤:
- 将堆顶元素与堆的最后一个元素进行交换。
- 删除堆中的最后一个元素。
- 将堆顶元素向下调整到满足堆的特性为止。
代码示例:
void AdjustDown(HPDataType* a, size_t size, size_t root){
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
//选出左右孩子小的那一个
if (child + 1 < size && a[child + 1] < a[child])
{
child++;
}
//向下调整,如果孩子小于父亲,则交换,继续向下调整
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(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);
}
四:二叉树的链式结构及其实现
在学习二叉树的基本操作之前,我们先创建一个非常简单的二叉树。
代码如下:
typedef int BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;
BTNode* BuyBTNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
printf("malloc fail\n");
exit(-1);
}
node->data = x;
node->left = node->right = NULL;
return node;
}
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyBTNode(1);
BTNode* node2 = BuyBTNode(2);
BTNode* node3 = BuyBTNode(3);
BTNode* node4 = BuyBTNode(4);
BTNode* node5 = BuyBTNode(5);
BTNode* node6 = BuyBTNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
注:从前文的概念可以看出,二叉树的定义是递归式的,因此后续的一系列操作均是按照该概念实现。
4.1 二叉树的遍历
二叉树的遍历是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。
按照规则,二叉树的遍历有:前序、中序、后序的递归结构遍历。
- 前序遍历:访问根节点的操作发生在遍历其左右子树之前。
- 中序遍历:访问根节点的操作发生在遍历其左右子树之间。
- 后序遍历:访问根节点的操作发生在遍历其左右子树之后。
4.1.1 前序遍历
递归图解:
引自比特科技数据结构课件
代码示例:
void PrevOrder(BTNode* root){
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL
4.1.2 中序遍历
void InOrder(BTNode* root){
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL
4.1.3 后序遍历
void LastOrder(BTNode* root){
if (root == NULL)
{
printf("NULL ");
return;
}
LastOrder(root->left);
LastOrder(root->right);
printf("%d ", root->data);
}
NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1
4.1.4 层序遍历
层序遍历属于广度优先遍历的一种。
思路:
- 先把跟入队列,借助队列先进先出的性质。
- 上一层节点出的时候,把相应的子节点入队列。
注意:我们此时队列中的值域存的节点指针。
void LevelOrder(BTNode* root){
Queue q;//创建个队列
QueueInit(&q);
if (root)
{
QueuePush(&q, root);//入跟节点
}
while (!QueueEmpty(&q))//队列不为空就持续进行!
{
BTNode* front = QueueFront(&q);//出对头结点
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
{
QueuePush(&q, front->left);//若子节点不为空,就入队列。
}
if (front->right){
QueuePush(&q, front->right);//若子节点不为空,就入队列。
}
}
printf("\n");
QueueDestory(&q);
}
更多见4.11
4.2 二叉树的创建和销毁
4.2.1 创建
例题链接:牛客链接
用输入的一串先序遍历字符串,根据此字符串建立一个二叉树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
#include<stdio.h>
typedef struct BinaryTreeNode
{
char data;
struct BinaryTreeNode * left;
struct BinaryTreeNode * right;
}BTNode;
BTNode * CreatTree(char * a, int * pi){
if(a[*pi] == '#'){
(*pi)++;
return NULL;
}
BTNode * root =(BTNode *) malloc(sizeof(BTNode));
root->data = a[(*pi)++];
root->left = CreatTree(a, pi);
root->right = CreatTree(a,pi);
return root;
}
void InOrder(BTNode * root){
if(root == NULL){
return;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
int main(){
char a[100];
scanf("%s", a);
int i = 0;
BTNode * tree = CreatTree(a, &i);
InOrder(tree);
return 0;
}
4.2.2 销毁
注意:销毁时采用后续遍历,因为这样才不会因销毁了父节点而找不到子节点的情况。
void BTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BTreeDestory(root->left);
BTreeDestory(root->right);
free(root);
}
谢谢大家!