✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog
1. 树
1.1. 树的定义
树是一种非线性的数据结构,它是由n(n >= 0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
在树中有一个特殊的结点,称为根结点,根节点没有前驱结点
除根节点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1 <= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
1.2. 树的基本概念
术语 | 定义 |
---|---|
节点的度 | 一个节点含有的子树的个数称为该节点的度,比如说节点1的度为2 |
叶节点 | 度为0的节点,比如说4,5,6节点 |
分支节点 | 度不为0的节点,比如说2,3节点 |
双亲节点 | 若一个节点含有子节点,则这个节点称为其子节点的父节点。比如说2是4,5的双亲节点 |
子节点 | 一个节点含有的子树的根节点称为该节点的子节点,比如说4,5是2的子节点 |
兄弟节点 | 具有相同父节点的节点互称为兄弟节点,比如说4,5就是兄弟节点 |
树的度 | 一棵树中,最大的节点的度称为树的度,比如说上面这棵树的度为2 |
节点的层次 | 从根开始定义起,根为第1层,根的子节点为第2层,以此类推 |
树的高度或深度 | 树中节点的最大层次,比如说上面这棵树的高度为3 |
堂兄弟节点 | 双亲在同一层的节点互为堂兄弟,比如说5,6节点 |
节点的祖先 | 从根到该节点所经分支上的所有节点,比如说1就是所有节点的祖先 |
子孙 | 以某节点为根的子树中任一节点都称为该节点的子孙,比如说所有节点都是1的子孙 |
森林 | 由m(m>0)棵互不相交的树的集合称为森林 |
1.3. 树的表示方法
下面我们将采用三种方式表示下面这课树:
1.3.1. 双亲表示法
双亲表示法采用顺序表的方式存储,即用顺序表存储各个节点的数据,并且同时存储其双亲节点的下标。注意:根节点没有双亲节点,所以特别记为-1。
#define MAX_SIZE 10
typedef int DataType;
typedef struct Node
{
DataType data;//数据域
int parent; //双亲结点在数组中的位置下标
}Node;
typedef struct PTree
{
//存放树中所有结点
Node tnode[MAX_SIZE];
//当前结点个数
int n;
}PTree;
1.3.2. 树的孩子表示法
树的孩子表示法就是采用顺序表与链表结合的形式,用顺序表存储树的值与链表的头节点,而链表的头节点存储其孩子节点在顺序表中的下标,若没有则记为空(N)。
#define MAX_SIZE 10
#define DataType int
typedef struct ListNode
{
int child;
struct ListNode* next;
}ListNode;
typedef struct TNode
{
DataType data;
//孩子链表的头指针
ListNode* firstchild;
}TNode;
typedef struct PTree{
//存储结点的数组
TNode nodes[MAX_SIZE];
int n; //结点数量
}PTree;
1.3.3. 左孩子右兄弟表示法
最常用表示树的的方法就是左孩子右兄弟表示法,即定义两个指针,让左指针指向子节点,右指针指向兄弟节点。
如果没有节点,则都指向空。
typedef int DataType;
struct Node
{
struct Node* leftChild1; // 孩子结点
struct Node* rightBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
1.4. 树的实际应用
在linux环境下目录结构就是有一颗树构成,而在Windows环境下,目录许多内容并不交叉,所以是由森林构成。
2. 二叉树
2.1. 二叉树的定义
一棵二叉树是结点的一个有限集合,该集合可能为空,也可以由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
注意:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
2.2. 特殊的二叉树
2.2.1. 满二叉树
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是N=2k -1,则它就是满二叉树。
2.2.2. 完全二叉树
**完全二叉树:**完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.3. 二叉树的特点
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2i -1个结点。
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数N=1+2+4+…+2i -1=2h -1。
- 对任何一棵二叉树, 如果度为0其叶结点个数为N0, 度为2的分支结点个数为N2, 则有 N0=N2+1
证明如下:
假设节点总数为N,如果度为0其叶结点个数为N0, 度为1的分支结点个数为N1,度为2的分支结点个数为N2
节点总数:N=N0 + N1+ N2 又因为二叉树的边比节点树少1,所以N= N1 +2N2+1=>N0 + N1+ N2= N1 +2N2+1=>N0=N2+1
-
若规定根节点的层数为1,具有n个结点的满二叉树的深度,h =log2 (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否则无右孩子。
2.4. 二叉树的存储
2.4.1. 数组存储
我们可以通过双亲节点与子节点下标之间的映射关系将二叉树存储在数组中,如果节点为空,我们以INT_MAX
来标记。
2.4.2. 链式存储
除了数组存储,我们也可以链式存储二叉树。
typedef struct BinaryTreeNode
{
struct BinTreeNode* left; // 左孩子
struct BinTreeNode* right; // 右孩子
DataType data; // 当前节点值域
}BTree;
2.5. 二叉树的遍历
二叉树的遍历一般有四种方法:前序遍历,中序遍历,后序遍历,层序遍历。
2.5.1. 前序遍历
前序遍历:先遍历根节点,再依次遍历左子树,右子树。而遍历左子树,又要先遍历根节点,再依次遍历左子树,右子树…直至遍历到空树。
- 递归实现
void PreOrder(BTree*root)
{
if (root == NULL)
{
return;
}
printf("%d ", root->data);//根节点
PreOrder(root->left);//左子树
PreOrder(root->right);//右子树
}
- 非递归实现
非递归实现树的前序遍历我们需要借助栈这个数据结构,来达到与递归一样的深度优先遍历的目的。并且栈中存储元素为BTree*
。
typedef BTree* STDataType;
void PreOrder(BTree* root)
{
Stack s;
InitStack(&s);
BTree*p = root;// p为遍历指针
while (p || !IsEmpty(&s)) // 栈不为空或p不为空时循环
{
if(p!=NULL)//入栈
{
printf("%d ", p->data);//根节点
StackPush(&s, p);
p = p->left;
}
else//出栈,访问右子树
{
p = StackTop(&s);//访问原本双亲节点
StackPop(&s); // 栈顶元素出栈
p = p->right; //访问
}
}
}
2.5.2. 中序遍历
中序遍历:先遍历左子树,再依次遍历根节点,右子树。而遍历左子树,又要先遍历左子树,再依次遍历根节点,右子树…直至遍历到空树。
- 递归实现
void Inorder(BTree*root)
{
if (root == NULL)
{
return;
}
PreOrder(root->left);//左子树
printf("%d ", root->data);//根节点
PreOrder(root->right);//右子树
}
- 非递归实现
非递归实现树的中序遍历我们需要借助栈这个数据结构,来达到与递归一样的深度优先遍历的目的。并且栈中存储元素为BTree*
。
typedef BTree* STDataType;
void Inorder(BTree* root)
{
Stack s;
InitStack(&s);
BTree* p = root;// p为遍历指针
while (p || !IsEmpty(&s)) // 栈不为空或p不为空时循环
{
if (p != NULL)//入栈
{
StackPush(&s, p);
p = p->left;
}
else//出栈,访问右子树
{
p = StackTop(&s);//访问原本双亲节点
StackPop(&s); // 栈顶元素出栈
printf("%d ", p->data);
p = p->right; //访问
}
}
}
2.5.3. 后序遍历
后序遍历:先遍历左子树,再依次遍历右子树,根节点。而遍历左子树,又要先遍历左子树,再依次遍历右子树,根节点…直至遍历到空树。
- 递归实现
void Postorder(BTree*root)
{
if (root == NULL)
{
return;
}
PreOrder(root->left);//左子树
PreOrder(root->right);//右子树
printf("%d ", root->data);//根节点
}
- 非递归实现
非递归实现树的后序遍历我们需要借助栈这个数据结构,来达到与递归一样的深度优先遍历的目的。并且栈中存储元素为BTree*
。
void Postorder(BTree* root)
{
Stack s;
InitStack(&s);
BTree* p = root;// p为遍历指针
BTree* v = root;// v标记已访问节点
while (p || !IsEmpty(&s)) // 栈不为空或p不为空时循环
{
while(p != NULL)//入栈
{
StackPush(&s, p);
p = p->left;
}
p = StackTop(&s);//访问双亲节点
if (p->right && p->right != v)//存在右子树,且没有被访问
{
p = p->right;//访问
}
else//没有右子树或者右子树已被访问
{
printf("%d ", p->data);
v = p;//记录当前访问的节点
p = NULL;//防止重复访问左子树
StackPop(&s);// 栈顶元素出栈
}
}
}
2.5.4. 层序遍历
层序遍历顾名思义就是一层一层地遍历,这时就需要借助一个数据结构:队列来辅助实现。
void leverOrder(BTree* root, Queue* pq)
{
if (root == NULL)//为空直接返回
{
return;
}
QueuePush(pq, root);//插入第一个节点
while (!QueueEmpty(pq))//队列不为空
{
BTree* p = QueueFront(pq);
printf("%d ", p->val);
QueuePop(pq);
if (p->left != NULL)//带入左孩子
{
QueuePush(pq, p->left);
}
if (p->right != NULL)//带入右孩子
{
QueuePush(pq, p->right);
}
}
}