二叉树的概念
二叉树(binary tree)是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。
// 结点结构体定义
typedef struct BTNode {
char data;
struct BTNode* LChild, * RChild;
} BTNode, * BTree;
每个节点都有两个引用(指针),分别指向左子节点(left-child node)和右子节点(right-child node),该节点被称为这两个子节点的父节点(parent node)。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的左子树(left subtree),同理可得右子树(right subtree)。
在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树。如图 7-1 所示,如果将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
二叉树常见术语
- 根节点(root node):位于二叉树顶层的节点,没有父节点。
- 叶节点(leaf node):没有子节点的节点,其两个指针均指向
None
。 - 边(edge):连接两个节点的线段,即节点引用(指针)。
- 节点所在的层(level):从顶至底递增,根节点所在层为 1 。
- 节点的度(degree):节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
- 二叉树的高度(height):从根节点到最远叶节点所经过的边的数量。
- 节点的深度(depth):从根节点到该节点所经过的边的数量。
- 节点的高度(height):从距离该节点最远的叶节点到该节点所经过的边的数量。
二叉树的性质
二叉树有一些需要理解并且记住的性质,以便于我们更好地使用它。
二叉树性质1:在二叉树的第i层上最多有2^(i-1)个结点(i≥1)。
第一层是根结点,只有一个,所以2(1-1)=20=1。 第二层有两个,2(2-1)=21=2。 第三层有四个,2(3-1)=22=4。 第四层有八个,2(4-1)=2^3=8。二叉树性质2:深度为k的二叉树至多有2^k-1个结点(k≥1)。
注意这里一定要看清楚,是2k后再减去1,而不是2(k-1)。以前很多同学不能完全理解,这样去记忆,就容易把性质2与性质1给弄混淆了。 深度为k意思就是有k层的二叉树,我们先来看看简单的。 如果有一层,至多1=21-1个结点。 如果有二层,至多1+2=3=22-1个结点。 如果有三层,至多1+2+4=7=23-1个结点。 如果有四层,至多1+2+4+8=15=2^4-1个结点。二叉树性质3:对任何一棵二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n1为度是1的结点数。则树T结点总数n=n0+n1+n2
n = n0 + n1 + n2 (节点数 = 所有节点的个数)
n = 0×n0 + 1×n1 + 2×n2 + 1 (节点数 = 分支数+1)由这两个公式可以推导出n0 = n2 + 1
二叉树性质4:具有n个结点的完全二叉树的深度为|log(2^n)+1| (向下取整)。
由满二叉树的定义我们可以知道,深度为k的满二叉树的结点数n一定是2k-1。因为这是最多的结点个数。那么对于n=2k-1倒推得到满二叉树的深度为k=log2(n+1),比如结点数为15的满二叉树,深度为4。二叉树性质5:如果对一棵有n个结点的完全二叉树(其深度为|log(2^n)+1|)的结点按层序编号(从第一层到第层,每层从左到右),对任一结点i(1<=i<=n),有
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点。
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
二叉树的遍历方式
二叉树的遍历是指 从根结点出发,按照某种次序一次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树的遍历方式有很多,如果我们限制了从左到右的方式,那么主要分为以下四种:
- 前序遍历
- 中序遍历
- 后序遍历
- 层次遍历
接下来我们对这四种遍历进行系统的学习~~~
前序遍历
规则是:
若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
注意:这里用词和贴切~~明明在说前序遍历的规则,结果规则里又是前序遍历,没错 这就是递归!
使用递归来实现遍历操作是非常的简洁。
//前序遍历
void PreOrderTraverse(BTree T){
if(T == NULL)
return;
printf("%d",T->data);
PreOrderTraverse(T->LChild);
PreOrderTraverse(T->RChild);
}
中序遍历
第一次经过时不访问,等遍历完左子树之后再访问,然后遍历右子树
//中序遍历
void InOrderTraverse(BTree T){
if(T == NULL)
return;
InOrderTraverse(T->LChild);
printf("%d",T->data);
InOrderTraverse(T->RChild);
}
后序遍历
第一次和第二次经过时都不访问,等遍历完该节点的左右子树之后,最后访问该节点
//后序遍历
void PostOrderTraverse(BTree T){
if(T == NULL)
return;
PostOrderTraverse(T->LChild);
PostOrderTraverse(T->RChild);
printf("%d",T->data);
}
层次遍历
通过对树中各层的节点从左到右依次遍历,即可实现对正棵二叉树的遍历,此种方式称为层次遍历。
建立二叉树
说了这么多,我们应该如何建立二叉树呢?
我们有这样的规则:
- 按照输入的序列创建二叉树,分别递归的创建左子树和右子树
- 如遇到‘#’代表子树为空
于是递归实现的代码如下:
// 先序序列创建二叉树 void CreateTree(BTree* T) { // 注意这里使用二级指针,因为我们要修改指针的值 char ch; // 用 getchar() 来处理换行符或空格 scanf_s(" %c", &ch); // 注意:前面的空格可以跳过任何空白字符(包括换行符) if (ch == '#') *T = NULL; else { *T = (BTNode*)malloc(sizeof(BTNode)); if (*T == NULL) { // 检查内存分配是否成功 printf("Memory allocation failed!\n"); exit(1); // 终止程序 } (*T)->data = ch; CreateTree(&(*T)->LChild); CreateTree(&(*T)->RChild); } }
我们还需要及时释放内存,避免空间的浪费
void DestroyTree(BTree T) { if (T == NULL) return; DestroyTree(T->LChild); DestroyTree(T->RChild); free(T); // 释放当前节点的内存 }
完整二叉树代码
#include <stdio.h>
#include <stdlib.h>
// 二叉链表的实现
// 结点结构体定义
typedef struct BTNode {
char data;
struct BTNode* LChild, * RChild;
} BTNode, * BTree;
// 前序遍历
void PreOrderTraverse(BTree T) {
if (T == NULL)
return;
printf("%c", T->data);
PreOrderTraverse(T->LChild);
PreOrderTraverse(T->RChild);
}
// 中序遍历
void InOrderTraverse(BTree T) {
if (T == NULL)
return;
InOrderTraverse(T->LChild);
printf("%c", T->data);
InOrderTraverse(T->RChild);
}
// 后序遍历
void PostOrderTraverse(BTree T) {
if (T == NULL)
return;
PostOrderTraverse(T->LChild);
PostOrderTraverse(T->RChild);
printf("%c", T->data);
}
// 先序序列创建二叉树
void CreateTree(BTree* T) { // 注意这里使用二级指针,因为我们要修改指针的值
char ch;
// 用 getchar() 来处理换行符或空格
scanf_s(" %c", &ch); // 注意:前面的空格可以跳过任何空白字符(包括换行符)
if (ch == '#')
*T = NULL;
else {
*T = (BTNode*)malloc(sizeof(BTNode));
if (*T == NULL) { // 检查内存分配是否成功
printf("Memory allocation failed!\n");
exit(1); // 终止程序
}
(*T)->data = ch;
CreateTree(&(*T)->LChild);
CreateTree(&(*T)->RChild);
}
}
// 释放二叉树的内存
void DestroyTree(BTree T) {
if (T == NULL)
return;
DestroyTree(T->LChild);
DestroyTree(T->RChild);
free(T); // 释放当前节点的内存
}
int main() {
BTree root = NULL; // 根节点初始化为 NULL
printf("输入先序序列来创建二叉树('#'表示空节点):\n");
CreateTree(&root); // 创建二叉树
printf("\n先序遍历结果:\n");
PreOrderTraverse(root);
printf("\n中序遍历结果:\n");
InOrderTraverse(root);
printf("\n后序遍历结果:\n");
PostOrderTraverse(root);
// 释放二叉树的内存
DestroyTree(root);
return 0;
}
测试结果: