二叉树从入门到精通:概念、结构与核心实现全解析

二叉树作为经典的非线性数据结构,是数据结构与算法学习的核心内容。本文从二叉树的基础概念出发,逐步讲解其核心性质、存储结构,并结合 C 语言代码实现二叉树的链式操作(遍历、结点统计、销毁等)。

一、树的基础概念铺垫

在学习二叉树之前,我们需要先了解的基本定义,因为二叉树是树的特殊形式,掌握树的概念能帮助我们更好地理解二叉树的特性。

1.1 树的定义

树是由n(n≥0)个有限结点组成的层次关系集合,形似倒挂的树(根朝上、叶朝下),核心特点:

  • 有且仅有一个根结点,根结点无前驱;
  • 除根外,每个结点有且仅有一个父结点,可以有 0 个或多个后继;
  • 子树之间互不相交,一棵N个结点的树必有N-1条边;
  • 树的定义是递归的,即每个子树也都是一棵独立的树。

1.2 树的核心术语

为了后续描述方便,先明确树的常用术语(这些术语同样适用于二叉树):

术语 定义
结点的度 一个结点含有的子树个数
叶结点(终端结点) 度为 0 的结点(无孩子)
分支结点(非终端结点) 度不为 0 的结点
树的度 一棵树中最大的结点的度
结点的层次 根为第 1 层,根的孩子为第 2 层,依次类推
树的高度 / 深度 树中结点的最大层次
森林 m(m>0)棵互不相交的树的集合

1.3 树的常用表示法

树的存储需要同时保存结点值结点间的关系 ,最常用的是孩子兄弟表示法,通过结构体实现:

cpp 复制代码
typedef int DataType;
struct Node
{
    struct Node* firstChild; // 指向第一个孩子结点
    struct Node* pNextBrother; // 指向下一个兄弟结点
    DataType data; // 结点数据域
};

这种表示法的优势是能将任意树结构转化为二叉树结构,也是二叉树能成为树结构核心的重要原因。

二、二叉树的核心概念与性质

二叉树是树的特殊形式,也是实际开发中最常用的树结构,其约束性更强,因此具备更多可利用的特性。

2.1 二叉树的定义

二叉树是结点的有限集合,满足以下两种情况之一:

  1. 集合为空(空二叉树);
  2. 由一个根结点 + 两棵互不相交的左子树和右子树组成,且左、右子树也都是二叉树。

核心约束

  • 二叉树中不存在度大于 2 的结点(每个结点最多有 2 个孩子);
  • 二叉树的子树有左右之分 ,次序不能颠倒(是有序树),比如只有左孩子和只有右孩子是两棵不同的二叉树。

2.2 两种特殊的二叉树

(1)满二叉树

如果二叉树的每一层结点数都达到最大值,则为满二叉树。

  • 若层数为K,则结点总数为2^K - 1
  • 满二叉树的每个分支结点都有左、右两个孩子,叶结点都在最下层。
(2)完全二叉树

完全二叉树由满二叉树引出,是效率最高的二叉树结构:

  • 对于深度为K、有n个结点的二叉树,每个结点都与深度为K的满二叉树中编号 1~n的结点一一对应;
  • 满二叉树是特殊的完全二叉树,但完全二叉树不一定是满二叉树;
  • 完全二叉树的叶结点仅出现在最下层和次下层,且最下层的叶结点都靠左排列。

2.3 二叉树的五大核心性质

  1. 非空二叉树的第i层上最多有 2^(i-1) 个结点
  2. 深度为h的二叉树最大结点数为 2^h - 1(满二叉树的结点数);
  3. 对任意二叉树,叶结点数n0 = 度为 2 的结点数n2 + 1
  4. 具有n个结点的满二叉树,深度h = log2(n+1)
  5. 具有n个结点的完全二叉树,按0 开始编号 ,对序号为i的结点:
    • 双亲结点序号:(i-1)/2i>0i=0为根,无双亲);
    • 左孩子序号:2i+12i+1 < n,否则无左孩子);
    • 右孩子序号:2i+22i+2 < n,否则无右孩子)。

性质 3 推导(关键)

  • 总结点数:N = n0 + n1 + n2n1为度为 1 的结点数);
  • 总边数:N-1 = n1 + 2*n2(度为 0 无边,度为 11 条边,度为 22 条边);
  • 联立两式,消去Nn1,得n0 = n2 + 1

2.4 二叉树的存储结构

二叉树有两种存储方式:顺序存储链式存储 ,二者各有适用场景,核心使用链式存储

(1)顺序存储

数组 存储二叉树,结点按层序遍历的顺序存入数组,仅适合完全二叉树

  • 优势:通过数组下标可直接找到结点的双亲、孩子,操作简单;
  • 劣势:非完全二叉树会出现大量空间浪费(空结点也要占数组位置);
  • 实际应用:仅用于(特殊的完全二叉树),普通二叉树不使用。
(2)链式存储

链表 存储二叉树,每个结点保存数据域和指向左、右孩子的指针,是二叉树的主流存储方式,分为两种:

  • 二叉链 :包含数据域 + 左孩子指针 + 右孩子指针,最常用;
  • 三叉链 :包含数据域 + 左孩子指针 + 右孩子指针 + 双亲指针,适用于需要快速找双亲的场景(如红黑树)。
cpp 复制代码
typedef int BTDataType;
typedef struct BinaryTreeNode {
    BTDataType val; // 结点数据
    struct BinaryTreeNode* left; // 左孩子指针
    struct BinaryTreeNode* right; // 右孩子指针
}BTNode;

三、二叉树链式结构的实战实现(C 语言)

3.1 代码工程结构

为了代码的模块化和可读性,采用头文件 + 源文件的结构,共 4 个文件:

  • BinaryTree.h:二叉树的结构体定义和函数声明;
  • BinaryTree.c:二叉树所有操作的函数实现;
  • Queue.h:队列的结构体定义和函数声明(层序遍历需要队列);
  • Queue.c:队列的函数实现(链式队列)。

3.2 基础准备:队列的实现

二叉树的层序遍历 需要借助队列 实现(先进先出),此处实现一个链式队列 ,队列中存储的是二叉树结点的指针(BTNode*)。

(1)队列头文件Queue.h
cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

// 队列存储的是二叉树结点的指针
typedef struct BinaryTreeNode* QDataType;

// 队列结点结构
typedef struct QListNode {
    QDataType val;
    struct QListNode* next;
}QNode;

// 队列的头/尾指针 + 有效元素个数
typedef struct Queue {
    QNode* head;
    QNode* tail;
    int size;
}Queue;

// 队列操作声明
void QueueInit(Queue* q);        // 初始化
void QueuePush(Queue* q, QDataType data); // 入队
void QueuePop(Queue* q);         // 出队
QDataType QueueFront(Queue* q);  // 获取队头
bool QueueEmpty(Queue* q);       // 判断空
void QueueDestroy(Queue* q);     // 销毁
(2)队列源文件Queue.c
cpp 复制代码
#include"Queue.h"

// 初始化队列
void QueueInit(Queue* Q)
{
    assert(Q);
    Q->head = Q->tail = NULL;
    Q->size = 0;
}

// 创建队列结点
QNode* CreatNode(QDataType x)
{
    QNode* ret = (QNode*)malloc(sizeof(QNode));
    if (ret == NULL)
    {
        perror("malloc fail!");
        exit(1);
    }
    ret->next = NULL;
    ret->val = x;
    return ret;
}

// 队尾入队
void QueuePush(Queue* Q, QDataType x)
{
    QNode* NewNode = CreatNode(x);
    assert(Q);
    if (Q->head == NULL)
    {
        Q->head = Q->tail = NewNode;
    }
    else
    {
        Q->tail->next = NewNode;
        Q->tail = NewNode;
    }
    Q->size++;
}

// 队头出队
void QueuePop(Queue* Q)
{
    assert(Q);
    assert(Q->head); // 空队列不能出队
    QNode* next = Q->head->next;
    free(Q->head);
    Q->head = next;
    if (Q->head == NULL)
    {
        Q->tail = NULL;
    }
    Q->size--;
}

// 获取队头元素
QDataType QueueFront(Queue* Q)
{
    assert(Q);
    assert(Q->head);
    return Q->head->val;
}

// 判断队列是否为空
bool QueueEmpty(Queue* Q)
{
    assert(Q);
    return Q->head == NULL;
}

// 销毁队列
void QueueDestroy(Queue* Q)
{
    assert(Q);
    while (Q->size--)
    {
        QNode* next = Q->head->next;
        free(Q->head);
        Q->head = next;
    }
    Q->head = Q->tail = NULL;
}

3.3 二叉树的核心操作实现

(1)二叉树头文件BinaryTree.h
cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>

// 二叉树结点数据类型
typedef int BTDataType;
// 二叉链结点定义
typedef struct BinaryTreeNode {
    BTDataType val;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTNode;

// 二叉树操作声明
void PreOrder(BTNode* root);        // 前序遍历
void InOrder(BTNode* root);         // 中序遍历
void PostOrder(BTNode* root);       // 后序遍历
int BinaryTreeSize(BTNode* root);   // 结点总数
int BinaryTreeLeafSize(BTNode* root);// 叶子结点数
int BinaryTreeLevelKSize(BTNode* root, int k); // 第k层结点数
BTNode* BinaryTreeFind(BTNode* root, BTDataType x); // 查找值为x的结点
void LevelOrder(BTNode* root);      // 层序遍历
void BinaryTreeDestory(BTNode* root); // 销毁二叉树
bool BinaryTreeComplete(BTNode* root); // 判断是否为完全二叉树
(2)二叉树源文件BinaryTree.c

所有操作均基于递归思想 (二叉树的定义是递归的,递归操作最贴合其结构),层序遍历和判断完全二叉树基于队列实现。

cpp 复制代码
#include"BinaryTree.h"
#include"Queue.h"

// 前序遍历:根 -> 左 -> 右
void PreOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    printf("%d ", root->val); // 先访问根
    PreOrder(root->left);     // 再遍历左子树
    PreOrder(root->right);    // 最后遍历右子树
}

// 中序遍历:左 -> 根 -> 右
void InOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    InOrder(root->left);      // 先遍历左子树
    printf("%d ", root->val); // 再访问根
    InOrder(root->right);     // 最后遍历右子树
}

// 后序遍历:左 -> 右 -> 根
void PostOrder(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    PostOrder(root->left);    // 先遍历左子树
    PostOrder(root->right);   // 再遍历右子树
    printf("%d ", root->val); // 最后访问根
}

// 统计结点总数:递归累加左、右子树结点数 + 根结点
int BinaryTreeSize(BTNode* root)
{
    if (root == NULL)
    {
        return 0;
    }
    return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

// 统计叶子结点数:左、右孩子都为空的结点是叶子
int BinaryTreeLeafSize(BTNode* root)
{
    if (root == NULL)
    {
        return 0;
    }
    // 叶子结点,返回1
    if (root->left == NULL && root->right == NULL)
    {
        return 1;
    }
    // 非叶子,递归累加左、右子树的叶子数
    return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

// 统计第k层结点数:k=1时为根结点,否则递归找左、右子树的第k-1层
int BinaryTreeLevelKSize(BTNode* root, int k)
{
    if (root == NULL || k <= 0)
    {
        return 0;
    }
    if (k == 1)
    {
        return 1;
    }
    return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

// 查找值为x的结点:先找根,再找左子树,最后找右子树
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
    if (root == NULL)
    {
        return NULL;
    }
    // 找到目标,返回结点指针
    if (root->val == x)
    {
        return root;
    }
    // 左子树查找,找到则返回
    BTNode* ret = BinaryTreeFind(root->left, x);
    if (ret)
    {
        return ret;
    }
    // 左子树没找到,找右子树
    return BinaryTreeFind(root->right, x);
}

// 层序遍历:自上而下、自左至右逐层访问,借助队列实现
void LevelOrder(BTNode* root)
{
    Queue Q1;
    QueueInit(&Q1);
    // 根结点入队
    if (root != NULL)
    {
        QueuePush(&Q1, root);
    }
    // 队列非空则循环
    while (!QueueEmpty(&Q1))
    {
        BTNode* tree = QueueFront(&Q1); // 取队头结点
        QueuePop(&Q1);                  // 队头出队
        printf("%d ", tree->val);       // 访问结点
        // 左孩子存在则入队
        if (tree->left)
            QueuePush(&Q1, tree->left);
        // 右孩子存在则入队
        if (tree->right)
            QueuePush(&Q1, tree->right);
    }
    QueueDestroy(&Q1); // 销毁队列,避免内存泄漏
}

// 销毁二叉树:后序遍历销毁(先销毁孩子,再销毁根,避免野指针)
void BinaryTreeDestory(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    BinaryTreeDestory(root->left);
    BinaryTreeDestory(root->right);
    free(root); // 释放当前结点
}

// 判断是否为完全二叉树:层序遍历,遇到NULL后若还有非NULL结点,则不是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
    Queue Q1;
    QueueInit(&Q1);
    if (root != NULL)
    {
        QueuePush(&Q1, root);
    }
    while (!QueueEmpty(&Q1))
    {
        BTNode* tree = QueueFront(&Q1);
        QueuePop(&Q1);
        // 遇到NULL,停止入队,检查后续是否有非NULL结点
        if (tree == NULL)
        {
            while (!QueueEmpty(&Q1))
            {
                BTNode* tmp = QueueFront(&Q1);
                QueuePop(&Q1);
                // 存在非NULL结点,不是完全二叉树
                if (tmp != NULL)
                {
                    QueueDestroy(&Q1);
                    return false;
                }
            }
            break;
        }
        // 不管孩子是否为NULL,都入队
        QueuePush(&Q1, tree->left);
        QueuePush(&Q1, tree->right);
    }
    QueueDestroy(&Q1);
    return true;
}

3.4 测试代码:创建二叉树并验证操作

为了快速验证上述操作,我们手动创建一棵简单的二叉树,测试所有核心功能:

cpp 复制代码
#include"BinaryTree.h"

// 创建二叉树结点
BTNode* BuyNode(BTDataType x)
{
    BTNode* node = (BTNode*)malloc(sizeof(BTNode));
    if (node == NULL)
    {
        perror("malloc fail");
        exit(1);
    }
    node->val = x;
    node->left = node->right = NULL;
    return node;
}

// 手动创建一棵二叉树
BTNode* CreatBinaryTree()
{
    BTNode* node1 = BuyNode(1);
    BTNode* node2 = BuyNode(2);
    BTNode* node3 = BuyNode(3);
    BTNode* node4 = BuyNode(4);
    BTNode* node5 = BuyNode(5);
    BTNode* node6 = BuyNode(6);

    node1->left = node2;
    node1->right = node4;
    node2->left = node3;
    node4->left = node5;
    node4->right = node6;

    return node1;
}

int main()
{
    // 创建二叉树
    BTNode* root = CreatBinaryTree();
    printf("前序遍历:");
    PreOrder(root); // 1 2 3 4 5 6
    printf("\n中序遍历:");
    InOrder(root);  // 3 2 1 5 4 6
    printf("\n后序遍历:");
    PostOrder(root);// 3 2 5 6 4 1
    printf("\n层序遍历:");
    LevelOrder(root);// 1 2 4 3 5 6

    printf("\n结点总数:%d\n", BinaryTreeSize(root)); // 6
    printf("叶子结点数:%d\n", BinaryTreeLeafSize(root)); // 3(3、5、6)
    printf("第3层结点数:%d\n", BinaryTreeLevelKSize(root, 3)); // 3(3、5、6)

    // 查找值为5的结点
    BTNode* findNode = BinaryTreeFind(root, 5);
    if (findNode)
    {
        printf("找到值为5的结点,地址:%p\n", findNode);
    }

    // 判断是否为完全二叉树
    if (BinaryTreeComplete(root))
    {
        printf("该二叉树是完全二叉树\n");
    }
    else
    {
        printf("该二叉树不是完全二叉树\n");
    }

    // 销毁二叉树
    BinaryTreeDestory(root);
    root = NULL; // 避免野指针
    return 0;
}

运行结果

cpp 复制代码
前序遍历:1 2 3 4 5 6
中序遍历:3 2 1 5 4 6
后序遍历:3 2 5 6 4 1
层序遍历:1 2 4 3 5 6
结点总数:6
叶子结点数:3
第3层结点数:3
找到值为5的结点,地址:000002207856A2E0
该二叉树是完全二叉树

四、二叉树学习的核心技巧与理解

4.1 递归是二叉树的灵魂

二叉树的定义是递归的,因此绝大多数二叉树操作都可以用递归实现,递归的核心思路是:

  • 整棵树的问题 分解为根结点 + 左子树 + 右子树的子问题;
  • 递归的终止条件 是遇到空结点root == NULL);
  • 比如统计结点总数:整棵树的结点数 = 左子树结点数 + 右子树结点数 + 1(根结点)。

递归误区 :不要试图手动模拟递归的每一步(会绕晕),只需关注当前层的逻辑终止条件

4.2 遍历是二叉树操作的基础

二叉树的遍历是所有操作的前提,无论是结点统计、查找还是销毁,本质上都是对遍历的应用:

  • 前序 / 中序 / 后序遍历:基于深度优先搜索(DFS),核心是 "先深后广";
  • 层序遍历:基于广度优先搜索(BFS),核心是 "先广后深",需要借助队列实现;
  • 不同的遍历顺序决定了访问结点的时机,比如后序遍历适合销毁(先销毁孩子,再销毁根)。

4.3 完全二叉树的判断技巧

判断完全二叉树的核心思路是层序遍历 + NULL 检测

  1. 层序遍历二叉树,所有结点(包括 NULL)都入队
  2. 当遇到第一个 NULL 时,停止入队,开始检查队列剩余元素;
  3. 若剩余元素全为 NULL,则是完全二叉树;若存在非 NULL 结点,则不是。

这种方法的依据是:完全二叉树的空结点只会出现在层序遍历的最后

4.4 内存泄漏的注意事项

二叉树是动态分配的内存(malloc),使用完毕后必须手动销毁,销毁时注意:

  • 采用后序遍历的方式销毁:先销毁左、右子树,再销毁根结点;
  • 若先销毁根结点,会导致左、右子树的指针变为野指针,无法访问和销毁;
  • 队列使用完毕后也需要销毁,避免内存泄漏。

五、总结与学习建议

二叉树是数据结构的分水岭,学好二叉树能为后续学习树、红黑树、B 树、堆等结构打下坚实基础,总结核心要点:

  1. 掌握二叉树的定义和核心性质 ,尤其是n0 = n2 + 1和完全二叉树的编号规则;
  2. 熟练实现二叉树的四种遍历(前、中、后、层序),理解递归和队列的应用;
  3. 掌握二叉树的基本统计操作(结点数、叶子数、第 k 层结点数),核心是递归分解问题;
  4. 理解完全二叉树的判断二叉树的销毁,注意内存管理。

创作不易,如果这篇文章对你有帮助,欢迎点赞、收藏、关注!后续会持续更新数据结构与算法的干货内容,一起学习,一起进步~

相关推荐
第二只羽毛2 小时前
第四章 串
大数据·数据结构·c#
浅念-2 小时前
Linux 基础命令与核心知识点
linux·数据结构·c++·经验分享·笔记·算法·ubuntu
guojb8242 小时前
从0开始设计一个树和扁平数组的双向同步方案
前端·数据结构·vue.js
西西弟2 小时前
拓扑排序及关键路径(数据结构)
数据结构·c++
j_xxx404_2 小时前
蓝桥杯基础--进制转换
开发语言·数据结构·c++·算法·职场和发展·蓝桥杯
丶小鱼丶3 小时前
数据结构和算法之【堆】
java·数据结构
马猴烧酒.3 小时前
【面试八股|操作系统】操作系统常见面试题详解笔记
java·linux·服务器·网络·数据结构·算法·eclipse
im_AMBER3 小时前
Leetcode 146 爬楼梯 | 打家劫舍
数据结构·算法·leetcode
㓗冽3 小时前
2026.03.25(第一天)
数据结构