【数据结构】二叉树的概念、结构和实现

文章介绍了树的基本概念,包括节点、度、根节点和叶子节点等,详细阐述了树的实现方式,如双亲表示法、孩子表示法和孩子兄弟表示法。接着,文章转而讨论二叉树,定义了二叉树的概念,提到了满二叉树、完全二叉树等特殊类型,并给出了二叉树的链式存储和顺序存储方法。此外,还详细讲解了二叉树的先序、中序和后序遍历操作。
摘要由CSDN通过智能技术生成

目录

一、树的概念、结构和实现

1.1 树

1.2 树的相关概念

1.3 树的实现

1.4 树的存储结构

1.4.1 双亲表示法

1.4.2 孩子表示法

1.4.3 孩子兄弟表示法

二、二叉树的概念、结构和实现

2.1 二叉树的概念

2.2 特殊的二叉树

2.3 二叉树的性质

2.4 二叉树的顺序存储

2.4.1 链式存储

2.4.2 顺序存储

三、二叉树链式结构的实现

3.1 创建一棵二叉树

4.2 二叉树的遍历

 4.2.1 先序遍历

 4.2.2 中序遍历

 4.2.13 后序遍历

4.3 通过先序遍历的数组创建二叉树

4.4 二叉树的销毁


一、树的概念、结构和实现

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^{(i-1)}个结点。
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^{h}-1
3. 对任何一棵二叉树, 如果度为0其叶结点个数为 n_{0},度为2的分支结点个数为 n_{2} ,则有                  n_{0}=n_{2}+1
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度为 h=log_{2}(n+1)
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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值