目录
一、树的概念、结构和实现
1.1 树
树是一种非线性的数据结构,它是由若干个节点和连接节点的边组成的具有层次关系的集合。 通常,节点表示某种实体或概念,而边表示节点之间的关联关系。
1.2 树的相关概念
- 节点:树中的每个元素都称为节点,它包含一个数据元素和若干个子节点。
- 节点的度:一个节点含有的子树的个数称为该节点的度。
- 根节点:树的最顶层节点称为根节点,它没有父节点。
- 叶子节点:没有子节点的节点称为叶子节点。 (度为0的节点)
- 非终端节点或分支节点:度不为0的节点。
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点
- 双亲节点/父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。 一个节点可以有多个子节点,但只能有一个父节点。
- 树的度:一棵树中,最大的节点的度称为树的度。
- 深度:从根节点到最深层叶子节点的距离。 根的深度为1,根的子节点深度为2,以此类推。
- 高度:根节点到最深层叶子节点的边数。
- 子树:一个节点及其所有子孙节点组成的树称为子树。
- 兄弟节点:具有相同父节点的节点称为兄弟节点。
- 祖先节点和后代节点:一个节点的祖先节点是其父节点、父节点的父节点等等,一个节点的后代节点是其子节点、子节点的子节点等等。
- 路径:从一个节点到另一个节点的一条路径,路径上的所有节点都是从起点到终点的。
- 有序树和无序树:如果树中节点的子节点有顺序,则称为有序树; 否则称为无序树。
- 森林:由若干个树组成的集合称为森林。
注意:
- 树是一种递归的数据结构,每个子树都是独立的树,除根节点外,其余结点被分成若干子树。
- 树中任意两个节点之间都有唯一的一条路径,路径上的所有节点都是从根节点到叶子节点的。
- 树的特点是具有层次性,每个节点可以有多个子节点,但每个节点只能有一个父节点,节点之间不存在环。
- 树形结构中,子树之间不能有交集,否则就不是树形结构
1.3 树的实现
树的实现既要保存值域,也要保存结点和结点之间的关系,可以使用指针或者数组两种方式来表示。
- 指针实现
在指针实现中,每个节点包含一个数据元素和若干个指针,指向它的子节点。 根节点通过一个指针来表示。 如果一个节点没有子节点,则指针为空。 这种实现方式的优点是可以动态地添加和删除节点,但是需要消耗更多的内存。 - 数组实现
在数组实现中,使用一个一维数组来表示整棵树,每个节点包含一个数据元素和两个指针,一个指向它的左子节点,一个指向它的右子节点。 根节点的索引为0,它的左子节点的索引为1,右子节点的索引为2。 如果一个节点没有子节点,则指针为空。 这种实现方式的优点是节省内存,但是需要预先确定树的深度,不支持动态添加和删除节点。
树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。 其中比较常用的孩子兄弟表示法:
typedef int DataType;
struct Node
{
struct Node* FirstChild1; // 第一个孩子结点
struct Node* NextBrother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};
树的应用非常广泛,例如文件系统、数据库索引、编译器语法分析等。常见的树结构包括二叉树、平衡树、红黑树、B树等。
1.4 树的存储结构
树的存储结构有以下几种:
1. 双亲表示法:每个节点存储其父节点在数组中的下标。
2. 孩子表示法:每个节点存储其所有子节点的指针或下标。
3. 孩子兄弟表示法(二叉树表示法):每个节点存储其第一个子节点和右兄弟节点的指针或 下标。
1.4.1 双亲表示法
假设一组连续空间存储树的节点,在每个节点中,设置一个下标指示其双亲结点在数组中的位置。
typedef int DataType;
#define MAX_TREE_SIZE 100
typedef struct {
DataType val;
int parent; // 父节点在数组中的下标
} PTNode;
typedef struct {
PTNode nodes[MAX_TREE_SIZE];
int root; // 根节点在数组中的下标
int num_nodes; // 树中节点个数
} PTree;
这样的存储结构,可以很容易的通过节点的parent的下标找到它的双亲结点(时间复杂度O(1)),但是如果需要找到一个结点的子节点,就需要遍历整个数组(时间复杂度O(n))。
1.4.2 孩子表示法
由于树中每个节点可能有多棵子树,所以可以让每个节点设置多个指针域,每个指针指向一棵子树的根节点。
在节点中建立一个位置用来存储指针域的个数,指针域的个数等于该节点的度。
这种方法克服了浪费空间的缺点,对空间利用率是很高了,但是由于各个结点是不同的结构,而且要输入结点的度的数值,就会在运算上带来时间上的损耗。
以上的方法中,每个节点的孩子数量不确定,所以我们对每个节点建立一个单链表体现它们的关系。
typedef int DataType;
#define MAX_TREE_SIZE 100
typedef struct CTNode {
int child; // 子节点在数组中的下标
struct CTNode* next; // 下一个兄弟节点
} CTNode;
typedef struct {
DataType val;
CTNode* children; // 子节点链表的头指针
} CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int root; // 根节点在数组中的下标
int num_nodes; // 树中节点个数
} CTree;
1.4.3 孩子兄弟表示法
任意—棵树,它的结点的第1个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针分别指向该结点的第一个孩子和此结点的右兄弟。
typedef struct CSNode {
DataType val;
struct CSNode* first_child; // 第一个子节点
struct CSNode* right_sibling; // 右兄弟节点
} CSNode, * CSTree;
二、二叉树的概念、结构和实现
2.1 二叉树的概念
二叉树是一种特殊的树,每个节点最多只能有两个子节点。通常将左子节点称为左子树,右子节点称为右子树,因此二叉树不存在度大于2的结点。二叉树是有序树,二叉树的子树有左右之分,次序不能颠倒。
2.2 特殊的二叉树
二叉树有几种不同的变体,包括满二叉树、完全二叉树和斜二叉树。
- 满二叉树是一种二叉树,其中每个节点都有两个子节点,除了叶子节点。
- 完全二叉树是一种二叉树,其中所有层从左到右都是满的,只有最后一层可以不是满的。
- 斜二叉树是一种二叉树,其中所有节点都只有一个子节点,要么是左子节点,要么是右子节点。
2.3 二叉树的性质
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 个结点。
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 。
3. 对任何一棵二叉树, 如果度为0其叶结点个数为 ,度为2的分支结点个数为 ,则有 。
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度为 。
5. 对于具有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 链式存储
链式存储是指用指针来表示二叉树中节点之间的关系。每个节点包含三个部分:数据域、左子树指针和右子树指针。可以用一个结构体来表示二叉树节点:
typedef int BTDataType;
typedef struct TreeNode {
BTDataType val;
TreeNode* left;
TreeNode* right;
}TreeNode;
通过指针的方式,可以方便地遍历二叉树。
2.4.2 顺序存储
顺序存储是指将二叉树的节点按照某种规律存储在数组中。具体来说,可以按照层次遍历的顺序将节点存储在数组中,根节点存储在下标为1的位置,其左子节点存储在下标为2的位置,右子节点存储在下标为3的位置,以此类推。如果某个节点没有左子节点或右子节点,相应的数组位置就为空。
- 一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。
- 顺序存储的优点是可以节省指针的空间,但是在插入、删除节点时需要进行数组元素的移动,效率较低。因此,链式存储更为常用。
- 二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
完全二叉树的顺序存储
父子间下标关系:
- 父亲下标找孩子:leftChild = parent * 2 + 1;
rightChild = parent * 2 + 2;- 孩子下标找父亲:parent = (child - 1) /2;
三、二叉树链式结构的实现
3.1 创建一棵二叉树
typedef char BTDataType;
typedef struct TreeNode {
DataType val;
TreeNode* left;
TreeNode* right;
}TreeNode;
TreeNode* BuyNode(DataType x)
{
TreeNode* tmp = (TreeNode*)malloc(sizeof(TreeNode));
if (tmp == NULL)
{
perror("malloc error!");
return;
}
tmp->val = x;
tmp->left = NULL;
tmp->right = NULL;
return tmp;
}
TreeNode* CreatBTree()
{
TreeNode* A = BuyNode('A');
TreeNode* B = BuyNode('B');
TreeNode* C = BuyNode('C');
TreeNode* D = BuyNode('D');
TreeNode* E = BuyNode('E');
TreeNode* F = BuyNode('F');
TreeNode* G = BuyNode('F');
TreeNode* H = BuyNode('H');
TreeNode* I = BuyNode('I');
A->left = B;
A->right = C;
B->left = D;
B->right = E;
C->left = F;
C->right = G;
D->left = H;
D->right = I;
return A;
}
以上并不是真正创建二叉树的方式,只是助于理解。真正的创建二叉树的方式在讲完遍历之后。
4.2 二叉树的遍历
二叉树的遍历就是访问每个节点,且每个节点只访问一次。根据根节点的访问顺序分为三种遍历方式:先序遍历、中序遍历和后序遍历。
4.2.1 先序遍历
先序遍历:访问根节点,然后按照“左孩子-右孩子”的顺序遍历子树。所以先序遍历结果为:
A B D H # # I # # E # # C F # # G # #。(访问到空树 输出'#')
即 ABDHIECFG.
// 先序遍历
void PreOrderTraversal(TreeNode* root) {
if (root != NULL) {
printf("%c ", root->val);
PreOrderTraversal(root->left);
PreOrderTraversal(root->right);
}
}
4.2.2 中序遍历
中序遍历:先访问根节点的左子树,然后访问根节点,最后再访问根节点的右子树。所以中序遍历
结果为:# H # D # I # B # E # A # F # C # G #。即 HDIBEAFCG.
// 中序遍历
void InOrderTraversal(TreeNode* root) {
if (root != NULL) {
InOrderTraversal(root->left);
printf("%c ", root->val);
InOrderTraversal(root->right);
}
}
4.2.13 后序遍历
后序遍历:先访问根节点的左右子树,最后访问根节点。所以后序遍历结果为:
# # H # # I D # # E B # # F # # G C A。即 HIDEBFGCA.
// 后序遍历
void PostOrderTraversal(TreeNode* root) {
if (root != NULL) {
PostOrderTraversal(root->left);
PostOrderTraversal(root->right);
printf("%c ", root->val);
}
}
4.3 通过先序遍历的数组创建二叉树
//二叉树创建
TreeNode* BTreeCreate(DataType* a, int* pi)
{
// 通过前序遍历的数组"ABDH##I##E##CF##G##"构建二叉树
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
TreeNode* node = BuyNode(a[*pi]);
(*pi)++;
node->left = BTreeCreate(a, pi);
node->right = BTreeCreate(a, pi);
return node;
}
//test.c
int main()
{
char arr[] = "ABDH##I##E##CF##G##";
int i = 0; //用来遍历数组,每次创建需要置为0
TreeNode* root = BTreeCreate(arr,&i);
PreOrderTraversal(root);
printf("\n");
InOrderTraversal(root);
printf("\n");
PostOrderTraversal(root);
return 0;
}
4.4 二叉树的销毁
在使用完二叉树后,为了避免内存泄漏,需要将其销毁。二叉树的销毁就是释放二叉树中每个节点的内存空间。因为二叉树使用的是动态内存分配,所以在不需要使用二叉树时,需要手动释放这些空间。
常见的二叉树销毁算法是递归实现的,递归地释放左右子树,再释放当前节点(后序遍历),具体实现代码如下:
//二叉树销毁
void DestoryBTree(TreeNode* root)
{
if (root == NULL)
{
return;
}
DestoryBTree(root->left);
DestoryBTree(root->right);
free(root);
root = NULL;
return;
}