目录
引言
在学习完栈和队列的之后后,我们接下来学习新的数据结构——树。
树
1.树的定义
树是一种非线性数据结构,它模拟了具有层次关系的数据的集合。树(数据结构)在形态上与自然界中的树木具有相似性,因此树才被称之为树,只不过它是倒挂着的树。
树中有一个特别的节点,称为根节点。它是整棵树的起点,没有前驱节点。
除根节点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1 <= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,树是递归定义的。
如下图所示:
注意:树形结构中,子树之间不能有交集,否则就不是树形结构。
例如这样:
子树是不相交的。
除了根节点外,每个节点有且仅有一个父节点。
一棵N个节点的树有N-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)棵互不相交的树的集合称为森林 |
3.树的表示方式
我们有三种方法表示下面这棵树:
(1)双亲表示法
双亲表示法是用顺序表,也就是数组对树进行表示的。即用顺序表存储各个节点的数据,并且同时存储其双亲节点的下标。
注意:根节点没有双亲节点,所以特别记为-1。
如下图所示:
存储方式如下:
说明:
1.因为根节点没有父节点,将其父节点数组下标设置为-1,根节点存储在顺序表的第1个位置。
2.数据元素2、3、4的父节点为0,父节点数组下标为0,分别存储在顺序表的2、3、4个位置。
3.数据元素5、6的父节点为1,父节点数组下标为1,分别存储在顺序表的第5、6个位置。
4.数据元素7的父节点为3,父节点数组下标为3,存储在顺序表的第7个位置。
优缺点:
优点:查找父节点方便
缺点:在查找子节点或兄弟节点时不够直接,通常需要遍历整棵树。
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 10
typedef int DataType;
typedef struct TreeNode
{
DataType data;
int parent;
}Node;
typedef struct ParentTree
{
// 数据结构的树节点
Node Tnode[MAXSIZE];
int n; //当前节点个数
}Ptree;
// 初始化树
void TreeInit(Ptree* ptree, int x, int parentIndex)
{
if (ptree->n >= MAXSIZE)
{
printf("Tree is full!\n");
return;
}
ptree->Tnode[ptree->n].data = x;
ptree->Tnode[ptree->n].parent = parentIndex;
ptree->n++; // 增加节点计数
}
(2)孩子表示法
树的孩子表示法就是采用顺序表与链表结合的形式,用顺序表存储树的值与链表的头节点,而链表的头节点存储其孩子节点在顺序表中的下标,若没有则记为空(NULL)。
相当于对树进行这样的处理:
存储方式如下:
说明:
添加一个数据(插入一个结点)
- 既要在数组中依次添加新的数据。
- 也要在其父节点后面用头插法插入。
头插法:数据结构——单链表
优缺点:
优点:
1.可以快速访问子节点。由于每个节点的子节点都被直接存储在链表中,因此可以快速访问任意节点的所有子节点。
2.易于插入和删除节点。当需要向树中插入或删除节点时,只需在相应的链表中添加或删除节点即可,操作相对简单。
缺点:
查找双亲节点不便。与双亲表示法相反,孩子表示法在查找节点的父节点时不太方便。通常需要从树的根节点开始遍历整个树或至少遍历该节点所在子树的一部分才能找到父节点。
代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 10
typedef int DataType;
typedef struct ListNode
{
int index;
struct ListNode* next;
}ListNode;
typedef struct TreeNode
{
DataType data;
ListNode* child;
}TNode;
typedef struct Tree
{
TNode nodes[MAXSIZE];
int n;
}Tree;
// 创建一个新的ListNode
ListNode* createListNode(int index)
{
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
if (newNode == NULL)
{
perror("malloc fail:");
exit(0);
}
newNode->index = index;
newNode->next = NULL;
return newNode;
}
// 初始化树
void TreeInit(Tree* t, DataType x)
{
if (t->n >= MAXSIZE)
{
printf("Tree is full!\n");
return;
}
t->nodes[t->n].data = x;
t->nodes[t->n].child = NULL;
t->n++;
}
// 向节点的子链表中添加一个新节点
void InsertChild(Tree* t, int parentIndex, int childIndex)
{
if (parentIndex >= t->n || childIndex >= t->n)
{
printf("error\n");
return;
}
ListNode* newNode = createListNode(childIndex);
if (newNode == NULL)
{
perror("malloc fail:");
return; // 内存分配失败
}
newNode->next = t->nodes[parentIndex].child;
t->nodes[parentIndex].child = newNode;
}
(3)左孩子右兄弟表示法
最常用表示树的的方法就是左孩子右兄弟表示法,即定义两个指针,让左指针指向子节点,右指针指向兄弟节点。如果没有节点,则都指向空(NULL)。
如图所示:
说明:
- 先找到在谁的后面插入,即找到父节点
- 然后再看看父节点的孩子指针空不空
- 空的话就插入到父节点的孩子指针域里,如果这个位置有结点的话(孩子指针域不空),就要插入到这个孩子结点的兄弟指针域里,插入方式选择头插法。
代码如下:
#include <stdio.h>
#include <stdlib.h>
typedef int DataType;
typedef struct TreeNode
{
DataType data;
struct TreeNode* leftChild; // 指向左孩子
struct TreeNode* rightBro; // 指向右兄弟
} TNode;
// 创建一个新的树节点
TNode* createTreeNode(DataType x)
{
TNode* newNode = (TNode*)malloc(sizeof(TNode));
if (newNode == NULL)
{
perror("malloc fail:");
exit(0);
}
newNode->data = x;
newNode->leftChild = NULL;
newNode->rightBro = NULL;
return newNode;
}
void InsertChild(TNode* t, DataType x, int isLeftChild)
{
if (t == NULL)
{
printf("Cannot insert into NULL node\n");
return;
}
TNode* newNode = createTreeNode(x);
if (newNode == NULL)
{
return;
}
// 如果isLeftChild为1,表示新节点应该作为左孩子插入
if (isLeftChild)
{
// 如果当前节点还没有右兄弟,则直接将新节点设置为右兄弟
if (t->leftChild == NULL)
{
t->leftChild = newNode;
}
// 如果当前节点已经有左孩子,则遍历左孩子的右兄弟链表
// 找到最后一个右兄弟,并将新节点插入为它的下一个右兄弟
else
{
TNode* tmp = t->leftChild;
while (tmp->rightBro != NULL)
{
tmp = tmp->rightBro;
}
tmp->rightBro = newNode;
}
}
// 如果isLeftChild为0,表示新节点应该作为右兄弟插入
else
{
// 如果当前节点还没有右兄弟,则直接将新节点设置为右兄弟
if (t->rightBro == NULL)
{
t->rightBro = newNode;
}
// 如果当前节点已经有右兄弟,则遍历右兄弟链表
// 找到最后一个右兄弟,并将新节点插入为它的下一个右兄弟
else
{
TNode* tmp = t->rightBro;
while (tmp->rightBro != NULL)
{
tmp = tmp->rightBro;
}
tmp->rightBro = newNode;
}
}
}
(4)树的应用
在linux环境下目录结构就是有一颗树构成,而在Windows环境下,目录许多内容并不交叉,所以是由森林构成。
结束语
下一篇我们将继续学习树的知识。
感谢各位大佬的支持!!!
求点赞收藏关注!!!