从零开始学二叉树(下):链式二叉树与递归的终极修炼

链式二叉树与递归详解

目录

🌟 引言:从“数组堆”到“链式树”——为什么还要学链式?

1️⃣ 链式二叉树:三个字段,撑起整棵树

🔧 结构定义(二叉链)

🛠️ 手动建树:从“画图”到“写代码”

2️⃣ 三大遍历:递归的灵魂三问

🔸 前序遍历(Preorder):根 → 左 → 右

🔸 中序遍历(Inorder):左 → 根 → 右

🔸 后序遍历(Postorder):左 → 右 → 根

3️⃣ 递归的本质:计算机如何“思考”遍历?

4️⃣ 常见操作:全部用递归实现!

(1)求节点总数

(2)求叶子节点数(度为 0)

(3)求第 k 层节点数

(4)求树的高度(深度)

(5)查找值为 x 的节点

(6)销毁二叉树(⚠️ 需要二级指针!)

5️⃣ 层序遍历:广度优先,用队列来帮忙

🧠 为什么需要队列?

6️⃣ 判断是否为完全二叉树(面试高频!)

🤔 完全二叉树的层序特征?

算法步骤:

7️⃣ LeetCode 入门题:思路引导,不是答案

🔸 题1:单值二叉树(LeetCode 965 )

题2:对称二叉树(LeetCode 101 )

✅ 本篇小结

💡 思考题(动手更深刻!)

🌈 终章寄语:树与递归,是数据结构的“成人礼”


作者:曾把 PostOrder 里写成两次 InOrder、调试到怀疑人生的东岸
目标读者:已经理解堆和完全二叉树、准备攻克链式二叉树的大一/大二同学
一句话预告:递归不是魔法,而是“分而治之”的思维艺术;遍历不是背代码,而是模拟计算机的思考路径。

栏目:《数据结构杂谈》 《C++入门》《C语言入门指南》《小游戏制作》

🌟 引言:从“数组堆”到“链式树”——为什么还要学链式?

前两篇,我们用数组 + 完全二叉树搞定了,效率高、实现快。
但现实中的树,真的都“完全”吗?

比如:

  • 一个公司架构图,CEO 下只有 2 个 VP,而某个 VP 下有 10 个经理
  • 一棵表达式树:+ 的左子是 *,右子是 5,而 * 下又有 23

这些树形状不规则,用数组存会浪费大量空间,甚至无法对齐。

于是,我们回到最灵活的表示法——链式二叉树

今天,我们将:

  • 手动构建一棵链式二叉树
  • 深入理解前/中/后序遍历的递归本质
  • 掌握求节点数、高度、查找、销毁等基础操作
  • 队列实现层序遍历,并判断是否为完全二叉树
  • 带你入门 LeetCode 经典题:单值树、对称树

准备好了吗?递归之旅,现在开始!

1️⃣ 链式二叉树:三个字段,撑起整棵树

🔧 结构定义(二叉链)

每个节点只关心三件事:

  • 自己存什么数据(val
  • 左孩子是谁(left
  • 右孩子是谁(right

C代码

typedef int BTDataType;

typedef struct BinaryTreeNode {
    BTDataType val;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
} BTNode;

    为什么叫“二叉链”
    因为每个结点有两个“链”(指针)连向孩子,像两条腿。

    🛠️ 手动建树:从“画图”到“写代码”

    我们构建如下这棵树:

            1
          /   \
         2     4
        /     / \
       3     5   6
            /
           7

    步骤

    1. 为每个值创建节点
    2. ->left / ->right 连起来
    BTNode* BuyNode(int val) {
        BTNode* node = (BTNode*)malloc(sizeof(BTNode));
        node->val = val;
        node->left = node->right = NULL;
        return node;
    }
    
    BTNode* CreateTree() {
        BTNode* n1 = BuyNode(1);
        BTNode* n2 = BuyNode(2);
        BTNode* n3 = BuyNode(3);
        BTNode* n4 = BuyNode(4);
        BTNode* n5 = BuyNode(5);
        BTNode* n6 = BuyNode(6);
        BTNode* n7 = BuyNode(7);
    
        n1->left = n2;   n1->right = n4;
        n2->left = n3;
        n4->left = n5;   n4->right = n6;
        n5->left = n7;
    
        return n1; // 返回根节点
    }

    💡 小建议
    初学时,先在纸上画树,再写代码连线。否则很容易搞反左右!

    2️⃣ 三大遍历:递归的灵魂三问

    记住
    所有二叉树的递归操作,都基于“根 + 左子树 + 右子树”这个结构定义

    🔸 前序遍历(Preorder):根 → 左 → 右

    访问顺序:先处理自己,再递归左右

    void PreOrder(BTNode* root) {
        if (root == NULL) {
            printf("N "); // 用 N 表示空,方便反推结构
            return;
        }
        printf("%d ", root->val);   // 1. 访问根
        PreOrder(root->left);       // 2. 遍历左子树
        PreOrder(root->right);      // 3. 遍历右子树
    }

    输出1 2 3 N N N 4 5 7 N N N 6 N N

    📌 关键每遇到一个节点,立刻打印

    🔸 中序遍历(Inorder):左 → 根 → 右

    访问顺序:先搞定左子树,再处理自己,最后右子树

    void InOrder(BTNode* root) {
        if (root == NULL) {
            printf("N ");
            return;
        }
        InOrder(root->left);        // 1. 左子树
        printf("%d ", root->val);   // 2. 访问根
        InOrder(root->right);       // 3. 右子树
    }

    输出N 3 N 2 N 1 N 7 N 5 N 4 N 6 N

    💡 应用:二叉搜索树(BST)的中序遍历 = 升序序列!

    🔸 后序遍历(Postorder):左 → 右 → 根

    访问顺序:先把左右孩子搞定,最后处理自己

    void PostOrder(BTNode* root) {
        if (root == NULL) {
            printf("N ");
            return;
        }
        PostOrder(root->left);      // 1. 左
        PostOrder(root->right);     // 2. 右
        printf("%d ", root->val);   // 3. 访问根
    }

    输出N N 3 N 2 N N N 7 N N 5 N N 6 4 1

    ⚠️ 踩坑经历
    我第一次写后序,手滑复制了 InOrder 两遍,结果死活不对。
    务必检查函数名

    3️⃣ 递归的本质:计算机如何“思考”遍历?

    很多同学背了遍历代码,但不知道为什么这样写
    我们以 PreOrder(1) 为例,模拟函数调用栈

    调用 PreOrder(1)
      → 打印 1
      → 调用 PreOrder(2)
          → 打印 2
          → 调用 PreOrder(3)
              → 打印 3
              → PreOrder(NULL) → 打印 N,返回
              → PreOrder(NULL) → 打印 N,返回
          → 调用 PreOrder(NULL) → 打印 N,返回
      → 调用 PreOrder(4)
          → 打印 4
          → 调用 PreOrder(5)
              → 打印 5
              → 调用 PreOrder(7) → 打印 7 → 两个 N
              → PreOrder(NULL) → N
          → 调用 PreOrder(6) → 打印 6 → 两个 N

    关键理解
    递归 = 重复“处理根 + 递归子树”,而系统用自动保存每次的状态。

    动手建议
    拿一张纸,手动展开递归过程。这是理解树递归的唯一捷径!

    4️⃣ 常见操作:全部用递归实现!

    (1)求节点总数

    int BinaryTreeSize(BTNode* root) {
        if (root == NULL) return 0;
        return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
    }

    📌 思维:总节点 = 自己 + 左子树节点数 + 右子树节点数

    (2)求叶子节点数(度为 0)

    int BinaryTreeLeafSize(BTNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right == NULL)
            return 1; // 是叶子
        return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
    }

    (3)求第 k 层节点数

    int BinaryTreeLevelKSize(BTNode* root, int k) {
        if (root == NULL || k < 1) return 0;
        if (k == 1) return 1; // 到目标层了
        // 否则:左子树第 k-1 层 + 右子树第 k-1 层
        return BinaryTreeLevelKSize(root->left, k - 1)
             + BinaryTreeLevelKSize(root->right, k - 1);
    }

    (4)求树的高度(深度)

    int BinaryTreeDepth(BTNode* root) {
        if (root == NULL) return 0;
        int leftH = BinaryTreeDepth(root->left);
        int rightH = BinaryTreeDepth(root->right);
        return (leftH > rightH ? leftH : rightH) + 1;
    }

    时间复杂度:O(n),每个节点访问一次

    (5)查找值为 x 的节点

    BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
        if (root == NULL) return NULL;
        if (root->val == x) return root;
    
        // 在左子树找
        BTNode* left = BinaryTreeFind(root->left, x);
        if (left) return left;
    
        // 左子树没找到,去右子树
        return BinaryTreeFind(root->right, x);
    }

    (6)销毁二叉树(⚠️ 需要二级指针!)

    void BinaryTreeDestroy(BTNode** root) {
        if (*root == NULL) return;
        BinaryTreeDestroy(&(*root)->left);
        BinaryTreeDestroy(&(*root)->right);
        free(*root);
        *root = NULL; // 避免野指针
    }

    为什么用二级指针
    因为要修改 root 本身(设为 NULL),而不是它指向的内容。

    5️⃣ 层序遍历:广度优先,用队列来帮忙

    前中后序是深度优先(一条路走到黑),
    层序遍历广度优先(一层一层扫)。

    🧠 为什么需要队列?

    思路
    把当前层的节点按顺序入队,出队时把它的孩子入队,这样自然就按层访问了。

    void LevelOrder(BTNode* root) {
        if (root == NULL) return;
    
        Queue q;
        QueueInit(&q);
        QueuePush(&q, root);
    
        while (!QueueEmpty(&q)) {
            BTNode* front = QueueFront(&q);
            printf("%d ", front->val);
            QueuePop(&q);
    
            if (front->left)  QueuePush(&q, front->left);
            if (front->right) QueuePush(&q, front->right);
        }
    
        QueueDestroy(&q);
    }

    输出1 2 4 3 5 6 7

    层序遍历 = BFS(广度优先搜索)

    6️⃣ 判断是否为完全二叉树(面试高频!)

    🤔 完全二叉树的层序特征?

    完全二叉树的层序遍历中,一旦出现 NULL,后面不能再有非 NULL 节点!

    算法步骤:

    1. 按层序遍历思路,连 NULL 也入队
    2. 一旦遇到 front == NULL跳出循环
    3. 检查队列剩余元素:如果还有非 NULL,就不是完全二叉树
    bool BinaryTreeComplete(BTNode* root) {
        if (root == NULL) return true;
    
        Queue q;
        QueueInit(&q);
        QueuePush(&q, root);
    
        while (!QueueEmpty(&q)) {
            BTNode* front = QueueFront(&q);
            QueuePop(&q);
    
            if (front == NULL) break; // 遇到空,停止入队
    
            // 即使孩子是 NULL,也入队!
            QueuePush(&q, front->left);
            QueuePush(&q, front->right);
        }
    
        // 检查剩余:不能有非空节点
        while (!QueueEmpty(&q)) {
            BTNode* node = QueueFront(&q);
            QueuePop(&q);
            if (node != NULL) {
                QueueDestroy(&q);
                return false;
            }
        }
    
        QueueDestroy(&q);
        return true;
    }

    📌 关键:NULL 之后不能有非 NULL,这是完全二叉树的“紧凑性”体现。 

    7️⃣ LeetCode 入门题:思路引导,不是答案

    🔸 题1:单值二叉树(LeetCode 965 

    题意:所有节点值都相同?

    递归思路

    • 空树 → true
    • 当前节点值 ≠ 左孩子值 → false
    • 当前节点值 ≠ 右孩子值 → false
    • 递归检查左右子树

    💡 边界:孩子为空时,不算“不等”

    题2:对称二叉树(LeetCode 101 

    题意:树是否左右镜像对称?

    核心思想
    不是比较左子树和右子树是否相等,而是比较“左的左” vs “右的右”、“左的右” vs “右的左”

    bool _isSymmetric(BTNode* left, BTNode* right) {
        if (!left && !right) return true;
        if (!left || !right) return false;
        if (left->val != right->val) return false;
        return _isSymmetric(left->left, right->right)
            && _isSymmetric(left->right, right->left);
    }
    
    bool isSymmetric(BTNode* root) {
        if (!root) return true;
        return _isSymmetric(root->left, root->right);
    }

    这是对递归思维的进阶考验

    ✅ 本篇小结

    遍历

    前/中/后序

    递归三段式,注意空指针

    节点数/高度

    递归分治

    总 = 左 + 右 + 自己

    查找/销毁

    递归 + 二级指针

    销毁要置 NULL

    层序遍历

    队列 + BFS

    连 NULL 也要处理(判断完全树时)

    完全二叉树判断

    层序 + NULL 检查

    NULL 后不能有非 NULL

    LeetCode 思维

    递归变形

    对称 = 镜像递归

    💡 思考题(动手更深刻!)

    1. 画递归树:对 PostOrder 遍历上面那棵 7 个节点的树,画出函数调用栈的变化过程。
    2. 反推结构:已知前序 1 2 3 N N N 4 N N,中序 N 3 N 2 N 1 N 4 N,画出原树。
    3. 改写非递归:尝试用手动实现中序遍历(进阶挑战!)。
    4. 完全树判断:以下层序序列是否为完全二叉树?
      • A: [1,2,3,4,5,6]
      • B: [1,2,3,4,null,5,6]
      • C: [1,2,3,null,4,5,6]

    🌈 终章寄语:树与递归,是数据结构的“成人礼”

    从文件系统到堆排序,从遍历到对称树,
    二叉树是我们第一次真正接触非线性结构
    递归是我们第一次真正理解分而治之

    你可能会觉得递归“绕”,但请相信:

    每一个递归高手,都曾被栈溢出折磨过

    多画图、多模拟、多调试,
    总有一天,你会看着一棵树,
    心里自动浮现出三种遍历结果,
    脑子里自然跳出递归代码。

    那时,你就真正“看见”了树。


    📢 三连收官
    如果这三篇博客帮到了你,
    点赞 + 收藏 + 关注
    是对我最大的支持!

              

    评论 18
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值