25、数据结构:树与二叉树的概念、特性及递归实现

数据结构:树与二叉树的概念、特性及递归实现

一、树的基础概念

树是由 n(n≥0)n(n\geq0)n(n≥0) 个结点组成的有限集合,核心定义与术语如下:

  • 空树 :n=0n=0n=0 时的特殊树;
  • 非空树 :有且仅有一个根结点 ,其余结点可分为 mmm 个互不相交的子树(子树本身也是树);
  • 结点的度:结点拥有的子树数量;
  • 叶结点/分支结点:度为0的结点称为叶结点(终端结点),度不为0的称为分支结点(非终端结点);
  • 树的度数:树中所有结点的最大度数;
  • 树的深度/高度:从根开始计数,根为第1层,其孩子为第2层,以此类推,最深结点的层数即为树的深度。

树的存储方式分为两种:

  1. 顺序结构:用数组存储(适合完全二叉树);
  2. 链式结构:用指针存储结点间的父子关系(通用方式)。

二、二叉树:特殊的树结构

二叉树是树的特殊形式,定义为:由 nnn 个结点组成的有限集合,要么是空树,要么由一个根结点和两棵互不相交的左子树、右子树组成(左、右子树也为二叉树)。

2.1 二叉树的核心特点

  1. 每个结点最多有2棵子树(左、右子树);
  2. 左、右子树是有顺序的,次序不能颠倒(即使只有一棵子树,也要区分左/右)。

2.2 特殊类型的二叉树

  • 斜树:所有结点只有左子树(左斜树)或只有右子树(右斜树),本质是退化的线性结构;
  • 满二叉树 :所有分支结点都有左、右子树,且所有叶结点在同一层(深度为 kkk 的满二叉树有 2k−12^k-12k−1 个结点);
  • 完全二叉树:按层序编号后,每个结点的编号与同深度的满二叉树对应位置编号一致(叶结点只出现在最后两层,且最后一层的结点靠左排列)。

2.3 二叉树的核心特性

  1. 第 iii 层最多有 2i−12^{i-1}2i−1 个结点(i≥1i\geq1i≥1);
  2. 深度为 kkk 的二叉树,最多有 2k−12^k-12k−1 个结点(k≥1k\geq1k≥1);
  3. 任意二叉树中,叶结点数 n0n_0n0 与度为2的结点数 n2n_2n2 满足:n0=n2+1n_0 = n_2 + 1n0=n2+1;
  4. 有 nnn 个结点的完全二叉树,深度为 ⌊log⁡2n⌋+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,左子树是 bb 的左是 d、右是 e),右子树是 cc 的右是 ff 的左是 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

四、总结

  1. 二叉树的核心是左/右子树的有序性,基于递归的创建与遍历是最直观的实现方式;
  2. 前序字符串法创建二叉树时,# 是关键的空结点标识,确保递归能正确终止;
  3. 遍历的本质是根结点的访问时机,前/中/后序的区别仅在于根结点的打印顺序;
  4. 递归销毁树时,需遵循"先子树后结点"的顺序,避免内存泄露。
相关推荐
Johny_Zhao4 小时前
OpenClaw安装部署教程
linux·人工智能·ai·云计算·系统运维·openclaw
董董灿是个攻城狮5 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
AI软著研究员12 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish12 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱13 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者1 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮1 天前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者1 天前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考1 天前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx1 天前
CART决策树基本原理
算法·机器学习