二叉树实战进阶全攻略:从层序遍历到OJ题深度解析

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划程序人生

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

二叉树实战进阶全攻略:从层序遍历到OJ题深度解析✨

你好!欢迎来到数据结构系列二叉树篇的第二篇实战进阶内容~

在上一篇中,我们夯实了树与二叉树的基础概念,掌握了前序、中序、后序三种深度优先遍历的核心逻辑。今天,我们将聚焦二叉树的实战核心技能,从节点计数、层序遍历等基础操作,到经典OJ题解、二叉树的销毁与构建,再到哈夫曼树的拓展知识,全方位提升你对二叉树的灵活运用能力。这些内容既是笔试高频考点,也是工程开发中处理树结构的必备技能🌳

准备好了吗?让我们一起将理论转化为实战能力,攻克二叉树的进阶关卡!🚀


文章摘要

本文为数据结构系列二叉树篇第二篇实战进阶内容,聚焦二叉树核心实战技能。详细讲解二叉树节点总数、叶子节点数的递归分治求解方法,深入剖析层序遍历的队列实现逻辑与核心思想。结合前序遍历数组存储、最大深度计算、平衡二叉树判断等经典OJ题,拆解解题思路并提供高效代码实现。补充二叉树的后序销毁方法、基于先序字符串的构建逻辑,以及哈夫曼树的贪心构建思想与编码规则,全方位提升二叉树实战能力。

阅读时长 :约30分钟
阅读建议

  1. 初学者:先掌握层序遍历的队列实现,再练习节点计数的递归写法
  2. 刷题备考者:重点记忆平衡二叉树、最大深度的解题模板,理解递归分治思想
  3. 面试冲刺者:熟练掌握基于先序字符串构建二叉树的逻辑,能独立手写代码
  4. 查漏补缺者:聚焦哈夫曼树的构建思想与编码规则,理解贪心算法的应用

一、知识回顾:二叉树的核心解题思想

在进入实战前,我们先回顾二叉树的两大核心解题思想,这是解决所有二叉树问题的关键:

  1. 递归分治思想:将整棵树的问题,拆分为根节点、左子树、右子树的子问题,递归求解子问题后合并结果(适用于深度优先遍历、节点计数等)。
  2. 辅助结构思想:利用栈、队列等线性结构,实现二叉树的非递归遍历或广度优先遍历(适用于层序遍历、非递归前序遍历等)。

核心原则:深度优先问题用递归分治,广度优先问题用队列辅助


二、实战一:二叉树节点计数(递归分治的经典应用)🔢

节点计数是二叉树最基础的实战操作,重点考察对递归分治思想的理解。

1. 求二叉树的节点总数

方法1:指针传参统计(不推荐)

通过传入整型指针修改外部变量,统计节点总数。缺点是依赖外部变量,违背高内聚的代码设计思想。

c 复制代码
void TreeSize(BTNode* root, int* psize)
{
    if (root == NULL)
        return;
    (*psize)++; // 节点非空,计数+1
    TreeSize(root->left, psize);  // 递归遍历左子树
    TreeSize(root->right, psize); // 递归遍历右子树
}
方法2:分治递归(推荐,面试首选)

核心思想:整棵树的节点数 = 根节点(1个) + 左子树节点数 + 右子树节点数。空树的节点数为0,递归求解左右子树的节点数即可。

c 复制代码
int TreeSize(BTNode* root)
{
    // 三目运算符简化代码:空树返回0,否则递归计算左右子树节点数之和+1
    return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

2. 求二叉树的叶子节点数

叶子节点定义 :度为0的节点,即leftright指针均为NULL的节点。
核心思想:整棵树的叶子节点数 = 左子树叶子节点数 + 右子树叶子节点数。若当前节点是叶子节点,直接返回1。

c 复制代码
int TreeLeafSize(BTNode* root)
{
    if (root == NULL)
        return 0; // 空树无叶子节点
    // 当前节点是叶子节点,返回1
    if (root->left == NULL && root->right == NULL)
        return 1;
    // 递归计算左右子树叶子节点数之和
    return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

💡 核心技巧 :二叉树的递归问题,本质是将大问题拆分为左右子树的小问题,明确递归终止条件和子问题合并方式,就能快速写出代码!


二、实战二:二叉树层序遍历(队列实现,广度优先遍历)📊

层序遍历是二叉树的广度优先遍历(BFS),核心思想是一层一层遍历,上一层节点带下一层节点,利用队列的先进先出(FIFO)特性实现。这是二叉树实战中的高频考点,必须熟练掌握。

1. 层序遍历的核心逻辑

  1. 初始化队列,若根节点非空,则将根节点入队。
  2. 循环判断队列是否为空:
    • 取出队头节点,访问该节点(打印、存储等操作)。
    • 若队头节点有左孩子,将左孩子入队;若有右孩子,将右孩子入队。
    • 队头节点出队。
  3. 队列空时,遍历结束,销毁队列避免内存泄漏。

2. 层序遍历的代码实现(结合之前实现的队列结构)

c 复制代码
void LevelOrder(BTNode* root)
{
    Queue q;
    QueueInit(&q); // 初始化队列

    // 根节点非空则入队,作为遍历的起点
    if (root)
        QueuePush(&q, root);

    while (!QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q); // 取出队头节点(当前层的节点)
        QueuePop(&q); // 队头节点出队

        printf("%c ", front->data); // 访问当前节点

        // 左孩子非空则入队,为下一层遍历做准备
        if (front->left)
            QueuePush(&q, front->left);
        // 右孩子非空则入队,注意左孩子先入队,保证层序遍历的顺序
        if (front->right)
            QueuePush(&q, front->right);
    }

    printf("\n");
    QueueDestroy(&q); // 销毁队列,避免内存泄漏
}

3. 层序遍历的经典练习

题目 :某完全二叉树按层序输出的序列为ABCDEFGH,求该树的前序序列。
解题思路 :根据层序序列构建完全二叉树,完全二叉树的节点编号(根为1)满足:左孩子编号为2*i,右孩子编号为2*i+1。构建完成后,进行前序遍历。
答案 :前序序列为ABDHECFG


三、实战三:二叉树经典OJ题深度解析(LeetCode高频考点)🔥

接下来,我们结合三道LeetCode经典题目,讲解二叉树的实战解题思路。这些题目覆盖了前序遍历的数组存储、最大深度计算、平衡二叉树判断,是笔试和面试的高频考点。

1. OJ题1:二叉树的前序遍历(LeetCode 144)

题目要求 :给定一个二叉树的根节点root,返回它的前序遍历节点值数组。数组需手动malloc分配空间,调用者负责free

解题思路

  1. 先递归计算二叉树的节点总数,确定数组的大小。
  2. 再递归进行前序遍历,将节点值存入数组中(使用指针传递数组下标,避免全局变量)。
  3. 返回数组,并通过returnSize参数返回数组大小。

代码实现

c 复制代码
// 辅助函数:递归计算二叉树的节点总数
int TreeSize(struct TreeNode* root)
{
    return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

// 辅助函数:前序遍历,将节点值存入数组
void preOrder(struct TreeNode* root, int* a, int* pi)
{
    if (root == NULL)
        return;
    a[*pi] = root->val; // 根节点值存入数组
    (*pi)++; // 数组下标后移,注意括号不能省略
    preOrder(root->left, a, pi); // 递归遍历左子树
    preOrder(root->right, a, pi); // 递归遍历右子树
}

// 主函数:前序遍历并返回数组
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
    *returnSize = TreeSize(root); // 获取数组大小
    int* a = (int*)malloc(*returnSize * sizeof(int)); // 分配数组空间
    int i = 0;
    preOrder(root, a, &i); // 前序遍历存入数组
    return a; // 返回数组
}

2. OJ题2:二叉树的最大深度(LeetCode 104)

题目要求:给定一个二叉树,求它的最大深度。最大深度是指从根节点到最远叶子节点的最长路径上的节点数。

解题思路(递归分治) :整棵树的最大深度 = max(左子树最大深度, 右子树最大深度) + 1。空树的最大深度为0,递归求解左右子树的最大深度即可。

代码实现

c 复制代码
int maxDepth(struct TreeNode* root)
{
    if (root == NULL)
        return 0; // 空树深度为0
    // 递归计算左右子树的深度
    int leftDepth = maxDepth(root->left);
    int rightDepth = maxDepth(root->right);
    // 返回左右子树深度的较大值 + 1(当前节点)
    return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}

3. OJ题3:平衡二叉树(LeetCode 110)

题目要求:给定一个二叉树,判断它是否是高度平衡的二叉树。高度平衡二叉树的定义:每个节点的左右两个子树的高度差的绝对值不超过1。

解题思路(递归分治)

  1. 空树是平衡二叉树。
  2. 非空树的平衡条件:
    • 当前节点的左右子树高度差的绝对值 ≤ 1。
    • 左子树是平衡二叉树。
    • 右子树是平衡二叉树。
  3. 利用之前实现的maxDepth函数计算子树高度。

代码实现

c 复制代码
// 辅助函数:求二叉树的最大深度
int maxDepth(struct TreeNode* root)
{
    if (root == NULL)
        return 0;
    int leftDepth = maxDepth(root->left);
    int rightDepth = maxDepth(root->right);
    return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}

// 主函数:判断是否是平衡二叉树
bool isBalanced(struct TreeNode* root)
{
    if (root == NULL)
        return true; // 空树是平衡二叉树
    // 计算当前节点左右子树的高度
    int leftDepth = maxDepth(root->left);
    int rightDepth = maxDepth(root->right);
    // 平衡条件:当前节点平衡,且左右子树都平衡
    return abs(leftDepth - rightDepth) < 2 
           && isBalanced(root->left) 
           && isBalanced(root->right);
}

四、实战四:二叉树的销毁与构建(工程必备技能)🛠️

1. 二叉树的销毁(后序遍历,必须掌握)

核心思想 :二叉树的销毁必须采用后序遍历 的顺序,先销毁左子树和右子树,再销毁根节点。若先销毁根节点,会导致无法访问左右子树,造成内存泄漏。
注意事项 :需使用二级指针 ,因为要修改根节点的指向,将其置为NULL,避免野指针。

代码实现

c 复制代码
void DestroyTree(struct TreeNode** root)
{
    // 边界条件:指针为空或根节点为空
    if (root == NULL || *root == NULL)
        return;
    // 递归销毁左子树
    DestroyTree(&(*root)->left);
    // 递归销毁右子树
    DestroyTree(&(*root)->right);
    // 销毁根节点
    free(*root);
    *root = NULL; // 置空,避免野指针
}

2. 基于先序字符串构建二叉树(面试高频题)

需求 :读入用户输入的先序遍历字符串(#代表空节点),构建一棵二叉树,然后进行中序遍历并输出结果。

核心思想 :递归构建二叉树。遇到#则返回空节点,否则构建根节点,再递归构建左子树和右子树(符合先序遍历的顺序:根→左→右)。

代码实现

c 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct TreeNode
{
    struct TreeNode* left;
    struct TreeNode* right;
    char val;
} TNode;

// 根据先序字符串创建二叉树,pi是数组下标指针,用于记录当前遍历的位置
TNode* CreateTree(char* a, int* pi)
{
    if (a[*pi] == '#') // #代表空节点
    {
        (*pi)++;
        return NULL;
    }
    // 构建当前根节点
    TNode* root = (TNode*)malloc(sizeof(TNode));
    if (root == NULL)
    {
        perror("malloc fail");
        exit(EXIT_FAILURE);
    }
    root->val = a[*pi];
    (*pi)++;
    // 递归创建左子树
    root->left = CreateTree(a, pi);
    // 递归创建右子树
    root->right = CreateTree(a, pi);
    return root;
}

// 中序遍历
void InOrder(TNode* root)
{
    if (root == NULL)
        return;
    InOrder(root->left);
    printf("%c ", root->val);
    InOrder(root->right);
}

int main()
{
    char str[100];
    scanf("%s", str);
    int i = 0;
    TNode* root = CreateTree(str, &i);
    InOrder(root);
    // 注意:实际工程中需要销毁二叉树,避免内存泄漏
    DestroyTree(&root);
    return 0;
}

五、拓展提升:哈夫曼树与哈夫曼编码(贪心算法的应用)📡

哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树。它在数据压缩、通信编码等领域有广泛的应用,核心思想是贪心算法

1. 哈夫曼树的构建思想

给定n个带权值的叶子节点,构建哈夫曼树的步骤如下:

  1. 将所有节点按权值从小到大排序。
  2. 取出权值最小的两个节点,构建一个新的父节点,父节点的权值为两个子节点权值之和。
  3. 将新父节点重新加入节点序列中,保持序列的有序性。
  4. 重复步骤2-3,直到序列中只剩下一个节点,该节点即为哈夫曼树的根节点。

2. 哈夫曼编码(前缀编码,无损压缩)

哈夫曼编码是基于哈夫曼树的一种编码方式,核心规则:

  • 左分支标记为0,右分支标记为1
  • 每个叶子节点的编码为从根节点到该节点的路径上的标记序列。
  • 权值越大的节点,编码越短(带权路径长度越短),从而实现数据压缩。

示例:给定权值为7(a)、5(b)、2©、4(d)的叶子节点,构建的哈夫曼编码如下:

  • a(权值7):编码为0(路径最短)
  • b(权值5):编码为10
  • c(权值2):编码为110
  • d(权值4):编码为111

3. 带权路径长度计算

带权路径长度(WPL)是指叶子节点的权值乘以其到根节点的路径长度(边数)。哈夫曼树的总带权路径长度是所有叶子节点的带权路径长度之和,且为最小值。

示例计算 :上述哈夫曼树的总带权路径长度 = 7×1 + 5×2 + 2×3 + 4×3 = 7 + 10 + 6 + 12 = 35


六、写在最后

恭喜你!二叉树篇的实战进阶内容至此圆满结束🎉~

从二叉树的节点计数、层序遍历,到经典OJ题解、二叉树的销毁与构建,再到哈夫曼树的拓展知识,你已经掌握了二叉树的核心实战技能,完成了从理论到应用的跨越:

  • 熟练掌握了二叉树的递归分治思想,能解决节点计数、最大深度等问题。
  • 掌握了层序遍历的队列实现,理解广度优先遍历的核心逻辑。
  • 能独立解决二叉树的经典OJ题,具备一定的算法解题能力。
  • 了解了二叉树的销毁与构建方法,掌握工程开发中的必备技能。
  • 初步认识了哈夫曼树与哈夫曼编码,理解贪心算法的应用。

二叉树是数据结构的"分水岭",也是算法面试的"高频考点"。建议你多敲几遍代码,多做几道OJ题,真正理解递归分治的核心思想------这不仅是解决二叉树问题的关键,更是解决所有复杂算法问题的基础。

下一个系列,我们将进入搜索二叉树与平衡树的世界,学习更高级的树结构,提升数据结构的应用能力~😜


点赞+收藏+关注,跟着系列内容一步步吃透数据结构!你的支持是我创作的最大动力~👍

相关推荐
散峰而望17 小时前
【算法竞赛】顺序表和vector
c语言·开发语言·数据结构·c++·人工智能·算法·github
千金裘换酒17 小时前
LeetCode 回文链表
算法·leetcode·链表
老鼠只爱大米17 小时前
LeetCode算法题详解 283:移动零
算法·leetcode·双指针·快慢指针·移动零·move zeroes
cpp_250118 小时前
B3927 [GESP202312 四级] 小杨的字典
数据结构·c++·算法·题解·洛谷
踩坑记录18 小时前
leetcode hot100 最长连续子序列 哈希表 medium
leetcode
Cx330❀18 小时前
《C++ 递归、搜索与回溯》第2-3题:合并两个有序链表,反转链表
开发语言·数据结构·c++·算法·链表·面试
独自破碎E18 小时前
链表相加(二)
数据结构·链表
CCPC不拿奖不改名18 小时前
面向对象编程:继承与多态+面试习题
开发语言·数据结构·python·学习·面试·职场和发展
꧁Q༒ོγ꧂18 小时前
算法详解(二)--算法思想基础
java·数据结构·算法