目录
目前,我们已经了解了一棵树的概念和三种表示方法,我们了解到树是一种有层次关系的,一对多的数据结构。我们来总结一下它的特点:
1 树的特点
- 每个结点有多个或零个子结点
- 没有父节点的结点为根结点
- 每个非根结点只有一个父节点
- 除根结点外,每个子结点可以分为多个不相交的子树
那今天,我们介绍一种特殊的树,名叫二叉树
2 二叉树的概念
⼆叉树是每个结点最多有两个子树的树结构。
这也就意味着
二叉树的每个结点的度不超过2
所以它有5种基本形态
- 空集
- 只有根结点(左右子树为空)
- 根和左子树(右子树为空)
- 根和右子树(左子树为空)
- 根和左右子树
其中,只有左子树或者右子树的树叫作斜树
特殊的二叉树
根据二叉树的定义,我们可以得到两种特殊的二叉树,分别是满二叉树和完全二叉树,而满二叉树又是一种特殊的完全二叉树。
满二叉树
- 所有的分支结点都存在左右子树,并且所有的叶子结点都在同一层
- 若深度为,结点数量为
顾名思义,满二叉树就是要将所有位置填满(
只可意会,不可言传)
如图,其深度为4,结点个数为15。
完全二叉树
- 若对⼀棵具有 个结点的⼆叉树按层序编号,如果编号为 的 结点 于同样深度的满⼆叉树中编号为 的结点在⼆叉树的位置相同的⼆叉树。
- 可理解为:生成结点的顺序严格按照从上到下,从左到右的顺序
- 特点:
- 结点没有左孩子一定没有没有右孩子
- 度为1的结点最多有一个
- 叶节点只能存在最下面两层
如图,结点5只有左孩子10,只有结点5的度为1,叶节点为6、7、8、9、10,位于最下面两层。
3 二叉树的性质
二叉树的每一条性质都很重要,理论考试(考研)都会针对性质出很多题。
性质 | 内容 |
1 | 在⼆叉树的层上至多有个结点() |
2 | 深度为 的⼆叉树至多有 个结点() |
3 | (:度为0的叶子结点,:度为2的结点) |
4 | 具有 个结点的完全⼆叉树的深度为 (向下取整) |
5 | 如果有⼀棵有 个结点的完全⼆叉树,按层序编号,则对任⼀结点 1.如果,则结点是⼆叉树的根,无双亲 2.如果,则其双亲是结点(向下取整) 3.如果 ,则结点 无左孩⼦,否则其左孩⼦是结点 4.如果 ,则结点无右孩⼦,否则其右孩⼦是结点 |
可能第三条性质比较突兀,而且证明思路与一般的树是相通的,我们来证明证明:
证明性质3
在⼀棵⼆叉树中,除了叶子结点(度为0)之外,就剩下度为2()和1()的结点了;
树的结点总数为,而连线数为;
所以有:(结点数减1=树的每个结点度数和)
整理得:
那对于一般的树呐?我们来看一道题:
设树T的度为4,其中度为1,2,3和4的结点个数分别为4,2,1,1 则T中的叶子数为
分析过程
已知:
- 树的度为4,即结点最大度为4,没有度为5、6...的结点;
- 设结点度为的结点个数为,;
- 结点总数:
- 树的总度数(每个结点度数和):
除了根结点,每个结点都对应着其父节点
伸出来的一个出度结点总数-1=树的总度数
4 二叉树的存储结构
4.1 顺序存储
由于⼆叉树的结点至多为2,因此这种性质使得⼆叉树是可以使⽤顺序存储结构来描述的。
在使用顺序存储结构时我们需要令数组的下标体现结点之间的逻辑关系。
我们先来看完全⼆叉树,如果我们按照从上到下,从左到右的顺序遍历完全⼆叉树时,顺序是这样的:
那么这个时候我们发现,如果设父结点的序号为,则子结点的序号分别为或者,子结点的序号和父结点都是相互对应的。
那对于一般的二叉树,我们也按这个逻辑存储,就会发现一个问题:
有三个位置没有存储元素,为空,浪费了空间。试想,若它是一棵斜树,那浪费的空间是不是更多呢?所以二叉树的顺序存储结构可行,但不合适。
4.2 链式存储
由于⼆叉树的每个结点最多只能有两个子树,因此我们就不需要使用上一节的3种表达法来做。
图解树的3种表示方法(含完整C代码)https://blog.csdn.net/weixin_54186646/article/details/123928713?spm=1001.2014.3001.5501可以直接设置⼀个结点具有两个指针域与⼀个数据域,那么这样就可以建好⼆叉树链表。
struct Node{
struct Node* left;
struct Node* right;
int key;//数据
};
这是二叉链表表示的方法,除此之外,还有三叉链表(在二叉链表基础上添加了父节点),双亲数组(同树的一般表示法中的双亲表示法),线索链表(之后再将)。
5 二叉树的遍历
5.1 递归遍历
递归遍历里面,我们根据输出的数据顺序不同,分成了三种遍历方式,分别为:
- 前序遍历:先访问根结点,然后左子树,然后右子树(中左右)
- 中序遍历:从根结点出发,先进⼊根结点的左子树中序遍历,然后访问根结点,最后访问右子树(左中右)
- 后序遍历:从左到右先叶子后结点的方式进入左右树遍历,最后访问根结点(左右中)
我们可以先看看代码
void Traverse(Node* root)
{
if (root == NULL)
{
return;
}
printf("%d", root->key);//先序遍历
Traverse(root->left);
//printf("%d", root->key);//中序遍历
Traverse(root->right);
//printf("%d", root->key);//后序遍历
}
前序遍历
遍历结果:1、2、4、9、5、10、3、7
上图按递归的思想写出了遍历顺序,但我们在做题中,不可能这么麻烦吧。
应试技巧
若让我们写出二叉树的前序遍历结果,我们一定牢记“中左右”三字诀,按下动图的方式写出
中序遍历
遍历结果:9、4、2、5、10、1、3、7
后序遍历
遍历结果:9、4、10、5、2、7、3、1
中序遍历与后序遍历的应试技巧同前序遍历,就不再赘述。
注:无论是什么样的遍历顺序,访问结点都是从根结点开始,按照从上到下,从左到右的顺序向下挖掘,分为 3 个顺序主要因为我们需要有⼀些方式来描述递归遍历的结果,让我们以抽象⼆叉树的结构,因此我们就按照输出语句放的位置不同而决定是什么序遍历。
根据以上内容,我们可以得出以下结论:
- 前序遍历的第一个元素一定为根节点,后序遍历的最后一个元素一定为根节点,中序遍历的根节点位置不易判断。
- 已知前序遍历与中序遍历结果,可以唯一确定后序遍历顺序;已知中序遍历与后序遍历结果,可以唯一确定前序遍历顺序;已知前序遍历与后序遍历结果,不能推出中序遍历结果。
(原因以及画出这棵树的方法,大家可以先想一想)
5.2 层序遍历
层序遍历不仅直观,而且好理解,但是我们要思考,处于同⼀层的结点存在于不同⼦树,按照刚才的递归遍历法我们无法和其他子树产⽣沟通,那该怎么实现?
仔细想想,层序遍历就好像从根结点开始,⼀层⼀层向下扩散搜索,这时我们想到了借助队列来实现:每访问一个结点(每出队一个元素),就让其左右孩子结点入队,然后按排队顺序继续出队。
遍历结果:1、2、3、4、5、7、9、10
我们直接上代码
我直接复制粘贴了队列部分的代码,创建树与查找结点的代码同孩子兄弟表示法,所以就没有将其改成left与right了。核心代码如下:
void levelOrder(Node* root)
{
Init_queue();
if (root != NULL)
{
insert_queue(root->key);
}
while (!isEmpty())
{
int temp = delete_queue();
get_node(root, temp);
printf("%d", temp);
if (tempNode->child!=NULL)
{
insert_queue(tempNode->child->key);
}
if (tempNode->sibling != NULL)
{
insert_queue(tempNode->sibling->key);
}
}
}
完整代码
#include<stdio.h>
#include<stdlib.h>
typedef struct ChildBro {
struct ChildBro* child;
struct ChildBro* sibling;
int key;//数据
}Node;
/*全局变量*/
int* queue;//用数组实现队列
int front;//头指针 因为是数组 这里用下标代表指针
int end;//尾指针 因为是数组 这里用下标代表指针
int maxSize;//当前的容量
void Init_queue()
{
queue = (int*)malloc(sizeof(int) * 100);
maxSize = 100;
front = end = 0;
}
int isEmpty()
{
if (front == end)//指针相遇 队空
{
return 1;
}
else {
return 0;
}
}
int isFull()
{
if ((end + 1) % maxSize == front)
{
return 1;
}
else
{
return 0;
}
}
/*
插入操作
int key : 待插入的关键字
*/
void insert_queue(int key)
{
if (isFull())
{
//队满 要么提示 要么扩容
}
else
{
end = (end + 1) % maxSize;
queue[end] = key;
}
}
int delete_queue()
{
if (isEmpty())
{
队空 提示
}
else
{
front = (front + 1) % maxSize;
return queue[front];
}
}
/*全局变量*/
Node* root;
Node* tempNode;
void Init(int);
void insert(int, int);
Node* get_node(Node*, int);
void Traverse(Node* root);
void levelOrder(Node* root);
int main()
{
Init(5);
insert(1, 5);
insert(2, 5);
insert(6, 1);
levelOrder(root);
}
void Init(int key) {
root = (Node*)malloc(sizeof(Node));
root->key = key;
root->child = NULL;
root->sibling = NULL;
}
void insert(int key, int parent)
{
//定位节点
get_node(root, parent);
if (tempNode == NULL)
{
//没有该节点
}
else
{
if (tempNode->child == NULL)
{
Node* node = (Node*)malloc(sizeof(Node));
node->key = key;
node->child = NULL;
node->sibling = NULL;
tempNode->child = node;
}
else
{
tempNode = tempNode->child;
Node* node = (Node*)malloc(sizeof(Node));
node->child = NULL;
node->sibling = NULL;
node->key = key;
node->sibling = tempNode->sibling;
tempNode->sibling = node;
}
}
}
Node* get_node(Node* node, int key)
{
if (node->key == key)
{
tempNode = node;
return tempNode;
}
if (node->sibling != NULL)
{
get_node(node->sibling, key);
}
if (node->child != NULL)
{
get_node(node->child, key);
}
}
void levelOrder(Node* root)
{
Init_queue();
if (root != NULL)
{
insert_queue(root->key);
}
while (!isEmpty())
{
int temp = delete_queue();
get_node(root, temp);
printf("%d", temp);
if (tempNode->child!=NULL)
{
insert_queue(tempNode->child->key);
}
if (tempNode->sibling != NULL)
{
insert_queue(tempNode->sibling->key);
}
}
}
今天先到这里,下一节是二叉排序树呢,还是二叉树与森林的转换呢?我再思考思考。感谢你的支持!