数据结构:树与二叉树的概念、特性及递归实现
一、树的基础概念
树是由 n(n≥0)n(n\geq0)n(n≥0) 个结点组成的有限集合,核心定义与术语如下:
- 空树 :n=0n=0n=0 时的特殊树;
- 非空树 :有且仅有一个根结点 ,其余结点可分为 mmm 个互不相交的子树(子树本身也是树);
- 结点的度:结点拥有的子树数量;
- 叶结点/分支结点:度为0的结点称为叶结点(终端结点),度不为0的称为分支结点(非终端结点);
- 树的度数:树中所有结点的最大度数;
- 树的深度/高度:从根开始计数,根为第1层,其孩子为第2层,以此类推,最深结点的层数即为树的深度。
树的存储方式分为两种:
- 顺序结构:用数组存储(适合完全二叉树);
- 链式结构:用指针存储结点间的父子关系(通用方式)。
二、二叉树:特殊的树结构
二叉树是树的特殊形式,定义为:由 nnn 个结点组成的有限集合,要么是空树,要么由一个根结点和两棵互不相交的左子树、右子树组成(左、右子树也为二叉树)。
2.1 二叉树的核心特点
- 每个结点最多有2棵子树(左、右子树);
- 左、右子树是有顺序的,次序不能颠倒(即使只有一棵子树,也要区分左/右)。
2.2 特殊类型的二叉树
- 斜树:所有结点只有左子树(左斜树)或只有右子树(右斜树),本质是退化的线性结构;
- 满二叉树 :所有分支结点都有左、右子树,且所有叶结点在同一层(深度为 kkk 的满二叉树有 2k−12^k-12k−1 个结点);
- 完全二叉树:按层序编号后,每个结点的编号与同深度的满二叉树对应位置编号一致(叶结点只出现在最后两层,且最后一层的结点靠左排列)。
2.3 二叉树的核心特性
- 第 iii 层最多有 2i−12^{i-1}2i−1 个结点(i≥1i\geq1i≥1);
- 深度为 kkk 的二叉树,最多有 2k−12^k-12k−1 个结点(k≥1k\geq1k≥1);
- 任意二叉树中,叶结点数 n0n_0n0 与度为2的结点数 n2n_2n2 满足:n0=n2+1n_0 = n_2 + 1n0=n2+1;
- 有 nnn 个结点的完全二叉树,深度为 ⌊log2n⌋+1\lfloor \log_2 n \rfloor + 1⌊log2n⌋+1。
三、二叉树的递归实现:从创建到遍历
以下代码基于链式结构 实现二叉树,通过前序字符串递归创建树,并实现前序、中序、后序遍历,最后递归销毁树避免内存泄露。
3.1 代码结构说明
二叉树结点结构体
c
typedef char DATATYPE;
// 二叉树结点:存储数据+左/右子树指针
typedef struct _treenode_
{
DATATYPE data;
struct _treenode_ *left; // 左子树指针
struct _treenode_ *right; // 右子树指针
} TREENODE;
3.2 递归创建二叉树(前序字符串法)
通过前序遍历序列 (用 # 表示空结点)递归构建二叉树,示例序列:"abd##e##c#fh###"(对应树结构见下文)。
c
char data[] = "abd##e##c#fh###"; // 前序序列(#表示空结点)
int inx = 0; // 全局变量:记录当前处理到序列的第几个字符
// 递归创建二叉树(传入根结点的指针的指针)
void CreateRoot(TREENODE **root)
{
char c = data[inx++]; // 取当前字符并移动下标
if ('#' == c)
{
*root = NULL; // #表示空结点
return;
}
// 分配结点内存并初始化
*root = (TREENODE *)malloc(sizeof(TREENODE));
if (NULL == *root)
{
printf("CreateRoot malloc failed\n");
return;
}
(*root)->data = c;
CreateRoot(&(*root)->left); // 递归创建左子树
CreateRoot(&(*root)->right); // 递归创建右子树
}
序列对应树结构 :
根结点为 a,左子树是 b(b 的左是 d、右是 e),右子树是 c(c 的右是 f,f 的左是 h),空结点用 # 填充。
3.3 深度优先遍历(前序、中序、后序)
深度优先遍历通过递归实现,核心是根结点的访问时机:
- 前序遍历:根 → 左 → 右
- 中序遍历:左 → 根 → 右
- 后序遍历:左 → 右 → 根
前序遍历
c
void PreOrderTraverse(TREENODE *T)
{
if (NULL == T) return;
printf("%c", T->data); // 先访问根
PreOrderTraverse(T->left); // 遍历左子树
PreOrderTraverse(T->right); // 遍历右子树
}
中序遍历
c
void InOrderTraverse(TREENODE *T)
{
if (NULL == T) return;
InOrderTraverse(T->left); // 先遍历左子树
printf("%c", T->data); // 再访问根
InOrderTraverse(T->right); // 最后遍历右子树
}
后序遍历
c
void PostOrderTraverse(TREENODE *T)
{
if (NULL == T) return;
PostOrderTraverse(T->left); // 先遍历左子树
PostOrderTraverse(T->right); // 再遍历右子树
printf("%c", T->data); // 最后访问根
}
3.4 二叉树的销毁(递归释放内存)
递归遍历所有结点,先释放左、右子树,再释放当前结点(避免野指针与内存泄露):
c
void DestroyTree(TREENODE *root)
{
if (NULL == root) return;
DestroyTree(root->left); // 销毁左子树
DestroyTree(root->right); // 销毁右子树
free(root); // 释放当前结点
}
3.5 主函数与运行结果
c
int main(int argc, char **argv)
{
TREENODE *root = NULL;
CreateRoot(&root); // 创建二叉树
printf("前序遍历:");
PreOrderTraverse(root);
printf("\n中序遍历:");
InOrderTraverse(root);
printf("\n后序遍历:");
PostOrderTraverse(root);
printf("\n");
DestroyTree(root); // 销毁树
return 0;
}
运行输出
前序遍历:abdecfh
中序遍历:dbeahfc
后序遍历:debhfca
四、总结
- 二叉树的核心是左/右子树的有序性,基于递归的创建与遍历是最直观的实现方式;
- 前序字符串法创建二叉树时,
#是关键的空结点标识,确保递归能正确终止; - 遍历的本质是根结点的访问时机,前/中/后序的区别仅在于根结点的打印顺序;
- 递归销毁树时,需遵循"先子树后结点"的顺序,避免内存泄露。