首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >数据结构(C语言篇):(十五)二叉树OJ题

数据结构(C语言篇):(十五)二叉树OJ题

作者头像
_OP_CHEN
发布2026-01-14 09:38:39
发布2026-01-14 09:38:39
460
举报
文章被收录于专栏:C++C++

前言

在学习完二叉树的一些相关函数的实现之后,相信大家都很想做一些题目来实战一下吧!本期博客,博主就为大家带来了一些优质好题,让我们一起来加深一下对二叉树的理解,现在正式开始吧!


一、单值二叉树

题目链接:https://leetcode.cn/problems/univalued-binary-tree/description/

1.1 题目分析

题目要求:判断一棵二叉树是否为「单值二叉树」—— 即树中所有节点的值都相同,是则返回true,否则返回false

题目还给出了如下的约束条件:

  • 树的节点数范围:[1, 100](根节点必存在,无需处理空树边界);
  • 节点值范围:[0, 99](整数类型)。

1.2 核心解题思路

单值二叉树的本质是“所有节点的值与根节点的值一致”,因此我们解题的核心思路是:

1. 确定基准值:以根节点的值为target(所有节点需与之相等);以根节点的值为target(所有节点需与之相等);

2. 遍历全树检查:通过「递归遍历」或「迭代遍历」(DFS/BFS)逐个检查节点值,若存在任意节点值≠target,直接返回false;全部检查通过则返回true

这里我们给出两种主流的遍历方式:

  • 递归:利用二叉树的递归结构(每个子树也是二叉树),代码简洁直观,适合题目节点数较少的场景;
  • 迭代:通过栈(DFS)或队列(BFS)手动维护遍历顺序,避免递归栈溢出风险(虽题目节点数少, but 通用解法)。

1.3 详细解题步骤与代码实现

1.3.1 前提:二叉树节点结构定义

LeetCode 中其实已经为我们预设了节点结构,这里为了清晰展示逻辑,补充定义如下:

代码语言:javascript
复制
// 二叉树节点结构
typedef struct TreeNode {
    int val;                // 节点值
    struct TreeNode *left;  // 左子节点
    struct TreeNode *right; // 右子节点
} TreeNode;
1.3.2 方法一:递归遍历
(1)解题步骤

1. 记录基准值:获取根节点的值target(所有节点需匹配此值);获取根节点的值target(所有节点需匹配此值)。

2. 定义递归检查函数:

  • 终止条件 1:若当前节点为NULL(空子树,无节点需检查)→ 返回true
  • 终止条件 2:若当前节点值≠target(找到不匹配节点)→ 返回false
  • 递归逻辑:当前节点匹配时,需同时递归检查左子树右子树(二者均需匹配才返回true)。

3. 调用递归函数:从根节点开始检查,返回最终结果。

(2)代码实现与详解
代码语言:javascript
复制
bool isUnivalTree(TreeNode* root) {
    // 步骤1:确定基准值(根节点值,题目保证root非空)
    int target = root->val;

    // 步骤2:定义递归辅助函数(检查当前节点及其子树)
    // 函数功能:判断以node为根的子树是否所有节点值为target
    bool check(TreeNode* node) {
        // 终止条件1:空节点,无需检查,返回true
        if (node == NULL) {
            return true;
        }
        // 终止条件2:节点值不匹配,返回false
        if (node->val != target) {
            return false;
        }
        // 递归检查左子树 AND 右子树(需两者都满足)
        return check(node->left) && check(node->right);
    }

    // 步骤3:从根节点开始检查,返回结果
    return check(root);
}
(3)关键细节
  • 递归的「短路特性」:若左子树检查返回false,则不会再递归检查右子树(直接返回false),能够大大提升效率。
  • 空节点返回true的原因:空子树是 “合法” 的(没有不匹配节点),例如示例 1 中根节点的右子树有一个null节点,是不影响结果的。
1.3.3 方法二:迭代遍历(BFS / 层序遍历)
(1)解题步骤

1. 记录基准值:同方法 1,获取根节点值target。

2. 初始化队列:用队列实现 BFS(层序遍历),先将根节点入队。

3. 循环遍历队列:

  • 出队头节点:取出当前要检查的节点;
  • 检查节点值:若≠target,直接返回false
  • 入队子节点:若左 / 右子节点非空,依次入队,确保后续检查。

4. 队列遍历结束:所有节点均匹配,返回true

(2)代码实现与详解
代码语言:javascript
复制
// 先实现队列的基础操作(适配TreeNode*类型)
typedef struct QueueNode {
    TreeNode* data;               // 队列存储二叉树节点指针
    struct QueueNode* next;       // 队列节点的下一个节点
} QueueNode;

typedef struct Queue {
    QueueNode* front; // 队头(出队)
    QueueNode* rear;  // 队尾(入队)
} Queue;

// 队列初始化:空队列,队头队尾均为NULL
void QueueInit(Queue* q) {
    q->front = q->rear = NULL;
}

// 入队:从队尾添加元素
void QueuePush(Queue* q, TreeNode* val) {
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    newNode->data = val;
    newNode->next = NULL;

    if (q->rear == NULL) { // 队列空时,队头队尾指向新节点
        q->front = q->rear = newNode;
    } else { // 队列非空时,队尾链接新节点并更新队尾
        q->rear->next = newNode;
        q->rear = newNode;
    }
}

// 出队:从队头删除元素(需确保队列非空)
void QueuePop(Queue* q) {
    if (q->front == NULL) return; // 空队列无需出队

    QueueNode* temp = q->front;   // 暂存队头节点
    q->front = q->front->next;    // 队头后移

    if (q->front == NULL) {       // 若出队后队列空,队尾也置为NULL
        q->rear = NULL;
    }
    free(temp); // 释放原队头节点的内存
}

// 判空:队列是否为空
bool QueueEmpty(Queue* q) {
    return q->front == NULL;
}

// 获取队头元素(需确保队列非空)
TreeNode* QueueFront(Queue* q) {
    return q->front->data;
}

// 销毁队列:释放所有队列节点内存
void QueueDestroy(Queue* q) {
    while (!QueueEmpty(q)) {
        QueuePop(q);
    }
}

// 核心:单值二叉树判断(BFS迭代版)
bool isUnivalTree(TreeNode* root) {
    if (root == NULL) return true; // 题目保证root非空,此处为通用处理

    // 步骤1:记录基准值
    int target = root->val;

    // 步骤2:初始化队列,根节点入队
    Queue q;
    QueueInit(&q);
    QueuePush(&q, root);

    // 步骤3:循环遍历队列
    while (!QueueEmpty(&q)) {
        // 3.1 出队头节点
        TreeNode* curr = QueueFront(&q);
        QueuePop(&q);

        // 3.2 检查当前节点值:不匹配则返回false
        if (curr->val != target) {
            QueueDestroy(&q); // 销毁队列避免内存泄漏
            return false;
        }

        // 3.3 子节点非空则入队(继续检查后续节点)
        if (curr->left != NULL) {
            QueuePush(&q, curr->left);
        }
        if (curr->right != NULL) {
            QueuePush(&q, curr->right);
        }
    }

    // 步骤4:所有节点检查通过,销毁队列并返回true
    QueueDestroy(&q);
    return true;
}
(3)关键细节
  • 队列的作用:确保按「层序」遍历所有节点,不遗漏任何一个;
  • 内存安全:每次返回前必须销毁队列(QueueDestroy),避免队列节点的内存泄漏;
  • 与递归的对比:迭代版无递归栈溢出风险,适合节点数极多的树(本题无需,但是是通用的)。

二、相同的树

题目链接:https://leetcode.cn/problems/same-tree/description/

2.1 题目分析

题目要求:判断两棵二叉树是否「相同」—— 需同时满足两个条件:① 结构完全一致(对应位置的节点存在性相同);② 对应节点的值完全相等,是则返回true,否则返回false

约束条件

  • 两棵树的节点数范围:[0, 100](允许空树,需处理 “两棵都空”“一棵空一棵非空” 的边界);
  • 节点值范围:[-10⁴, 10⁴](整数类型,需支持负数对比)。

2.2 核心解题思路

“相同树” 的本质是「两棵树的节点在 “位置” 和 “值” 上完全对齐」,因此核心思路是同步遍历两棵树,每一步对「对应位置的节点」做双重检查:

  1. 结构检查:判断当前位置的两个节点是否 “同时存在” 或 “同时不存在”(若一个存在、一个不存在,则结构不一致);
  2. 值检查:若两个节点均存在,判断其值是否相等(值不等则直接不相同)。

我们同样也可以分别使用递归和迭代两种遍历方式实现:

2.3 详细解题步骤与代码实现

2.3.1 递归遍历
(1)解题步骤

递归的核心是 “分而治之”:将 “判断整棵树相同” 拆解为 “判断根节点相同 + 左子树相同 + 右子树相同”,终止条件覆盖所有边界情况:

1. 边界条件处理:

  • 若两棵树的当前节点均为NULL(同时不存在)→ 结构一致,返回true
  • 若其中一棵的当前节点为NULL、另一棵非NULL(一存一失)→ 结构不一致,返回false。

2. 值检查:若两个节点均存在,但值不相等 → 返回false。

3. 递归深入:当前节点检查通过后,递归判断「p 的左子树与 q 的左子树」和「p 的右子树与 q 的右子树」(需两者均相同,故用 “与运算” 连接)。

(2)代码实现与详解
代码语言:javascript
复制
bool isSameTree(TreeNode* p, TreeNode* q) {
    // 步骤1:处理边界条件(结构检查)
    // 情况1:两棵树当前节点都为空 → 结构一致,返回true
    if (p == NULL && q == NULL) {
        return true;
    }
    // 情况2:一棵空、一棵非空 → 结构不一致,返回false
    if (p == NULL || q == NULL) {
        return false;
    }

    // 步骤2:值检查(均非空时对比值)
    if (p->val != q->val) {
        return false;
    }

    // 步骤3:递归深入左子树和右子树(需两者均相同才返回true)
    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
(3)关键细节
  • 终止条件的顺序:必须先判断 “是否为空”,再判断 “值是否相等”—— 若先判断值,会对NULL节点解引用(p->val),导致程序崩溃;
  • 递归的 “与运算” 逻辑:只有左子树和右子树同时相同,整棵树才相同。例如示例 3 中,左子树对比返回false,则无需再判断右子树,直接返回false(对短路进行的优化)。
2.3.2 迭代遍历(BFS / 层序遍历)
(1)解题步骤

迭代的核心是 “同步入队、同步出队”:用队列存储两棵树的「对应节点对」,每次取出一对节点做检查,再将它们的左、右子节点成对入队,确保遍历顺序完全同步:

1. 初始化队列:将两棵树的根节点pq作为 “第一对节点” 入队。

2. 循环处理队列:

  • 出队:取出队头的两个节点(currP为 p 的当前节点,currQ为 q 的当前节点);
  • 检查:按 “边界条件(结构)→ 值” 的顺序判断,若不满足直接返回false
  • 入队:将currP的左子节点与currQ的左子节点、currP的右子节点与currQ的右子节点成对入队(确保下一轮处理对应位置)。

3. 队列遍历结束:所有节点对均检查通过,返回true

(2)代码实现与详解(使用队列辅助)

首先实现适配 “节点对” 的队列(存储TreeNode*类型,每次处理两个元素):

代码语言:javascript
复制
// 队列节点结构(存储二叉树节点指针)
typedef struct QueueNode {
    TreeNode* data;
    struct QueueNode* next;
} QueueNode;

// 队列结构(队头出、队尾入)
typedef struct Queue {
    QueueNode* front;
    QueueNode* rear;
} Queue;

// 队列初始化:空队列,队头队尾均为NULL
void QueueInit(Queue* q) {
    q->front = q->rear = NULL;
}

// 入队:从队尾添加元素
void QueuePush(Queue* q, TreeNode* val) {
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    newNode->data = val;
    newNode->next = NULL;

    if (q->rear == NULL) { // 空队列时,队头队尾指向新节点
        q->front = q->rear = newNode;
    } else { // 非空队列时,链接新节点并更新队尾
        q->rear->next = newNode;
        q->rear = newNode;
    }
}

// 出队:从队头删除元素(需确保队列非空)
void QueuePop(Queue* q) {
    if (q->front == NULL) return;

    QueueNode* temp = q->front;
    q->front = q->front->next;

    if (q->front == NULL) { // 出队后为空,队尾置NULL
        q->rear = NULL;
    }
    free(temp); // 释放原队头内存
}

// 判空:队列是否为空
bool QueueEmpty(Queue* q) {
    return q->front == NULL;
}

// 获取队头元素(需确保队列非空)
TreeNode* QueueFront(Queue* q) {
    return q->front->data;
}

// 销毁队列:释放所有队列节点内存
void QueueDestroy(Queue* q) {
    while (!QueueEmpty(q)) {
        QueuePop(q);
    }
}

再实现核心的 “相同树判断” 逻辑:

代码语言:javascript
复制
bool isSameTree(TreeNode* p, TreeNode* q) {
    // 初始化队列,将p和q作为第一对节点入队
    Queue qNode;
    QueueInit(&qNode);
    QueuePush(&qNode, p);
    QueuePush(&qNode, q);

    // 循环处理队列中的节点对
    while (!QueueEmpty(&qNode)) {
        // 步骤1:出队一对节点(currP对应p的节点,currQ对应q的节点)
        TreeNode* currP = QueueFront(&qNode);
        QueuePop(&qNode);
        TreeNode* currQ = QueueFront(&qNode);
        QueuePop(&qNode);

        // 步骤2:结构检查(边界条件)
        if (currP == NULL && currQ == NULL) {
            continue; // 均为空,无需后续检查,跳过当前轮
        }
        if (currP == NULL || currQ == NULL) {
            QueueDestroy(&qNode); // 销毁队列避免内存泄漏
            return false; // 一存一失,结构不同
        }

        // 步骤3:值检查(均非空时对比值)
        if (currP->val != currQ->val) {
            QueueDestroy(&qNode);
            return false;
        }

        // 步骤4:将下一对节点入队(左子树对 + 右子树对)
        QueuePush(&qNode, currP->left);
        QueuePush(&qNode, currQ->left);
        QueuePush(&qNode, currP->right);
        QueuePush(&qNode, currQ->right);
    }

    // 所有节点对均检查通过
    QueueDestroy(&qNode);
    return true;
}
(3)关键细节
  • 节点对的入队顺序:必须严格按照 “p 的左 → q 的左 → p 的右 → q 的右” 入队,确保下一轮处理的是 “对应位置的子节点”—— 若顺序错误(如 p 的左和 q 的右入队),会导致结构对比错位。
  • 队列的内存安全:无论返回true还是false,都必须调用QueueDestroy释放队列内存,避免队列节点的内存泄漏。
  • 空节点的处理:当一对节点均为空时,用continue跳过后续检查(无需入队子节点,因为空节点没有子节点),避免无效操作。

三、对称二叉树

题目链接:https://leetcode.cn/problems/symmetric-tree/description/

3.1 题目分析

题目要求:判断一棵二叉树是否「轴对称」—— 即树的左子树与右子树呈「镜像对称」(结构一致且对称位置的节点值相等),是则返回true,否则返回false

约束条件

  • 树的节点数范围:[1, 1000](根节点必存在,需处理 “左 / 右子树为空” 的边界);
  • 节点值范围:[-100, 100](支持负数对比)。

3.2 核心解题思路

“对称二叉树” 的本质不是 “树与自身相同”,而是左子树与右子树镜像。镜像的核心条件是:

  1. 根值相等:左子树的根节点值 = 右子树的根节点值;
  2. 结构镜像:左子树的「左孩子」对应右子树的「右孩子」,左子树的「右孩子」对应右子树的「左孩子」;
  3. 递归满足:所有对称位置的子树均需满足上述两点。

下面我同样也用递归法和迭代法两种方法解题。

3.3 详细解题步骤与代码实现

3.3.1 方法一:递归法
(1)解题步骤

递归的核心是 “拆分镜像检查”:用辅助函数接收两个节点(左子树的某节点、右子树的对称节点),按以下逻辑判断:

1. 边界条件处理:

  • 若两个节点均为NULL(对称位置都空)→ 结构对称,返回true
  • 若一个节点为NULL、另一个非NULL(对称位置一存一失)→ 结构不对称,返回false。

2. 值检查:若两个节点均存在,但值不相等 → 不对称,返回false。

3. 递归深入:当前节点检查通过后,递归检查「左节点的左 vs 右节点的右」和「左节点的右 vs 右节点的左」(需两者均对称,用 “与运算” 连接)。

(2)代码实现与详解
代码语言:javascript
复制
// 辅助函数:检查两个节点是否镜像对称
bool isMirror(TreeNode* leftNode, TreeNode* rightNode) {
    // 步骤1:边界条件(结构检查)
    // 情况1:两个节点都为空 → 对称
    if (leftNode == NULL && rightNode == NULL) {
        return true;
    }
    // 情况2:一个空、一个非空 → 不对称
    if (leftNode == NULL || rightNode == NULL) {
        return false;
    }

    // 步骤2:值检查(均非空时对比值)
    if (leftNode->val != rightNode->val) {
        return false;
    }

    // 步骤3:递归检查镜像位置(左左对右右,左右对右左)
    return isMirror(leftNode->left, rightNode->right) 
           && isMirror(leftNode->right, rightNode->left);
}

// 主函数:判断树是否轴对称
bool isSymmetric(TreeNode* root) {
    // 根节点为空(题目约束节点数≥1,此处兼容空树)
    if (root == NULL) {
        return true;
    }
    // 检查根节点的左子树与右子树是否镜像
    return isMirror(root->left, root->right);
}
(3)关键细节
  • 辅助函数的必要性:主函数无法直接处理 “对称位置” 的节点,需辅助函数接收两个节点(左子树和右子树的对应节点);
  • 递归的镜像逻辑:比如示例 1 中,左子树的 2(根的左)与右子树的 2(根的右)对比后,需进一步检查 “左 2 的左(3)vs 右 2 的右(3)”“左 2 的右(4)vs 右 2 的左(4)”,完全匹配才对称;
3.3.2 方法二:迭代法(BFS / 队列实现)
(1)解题步骤

迭代的核心是 “同步处理对称节点对”:用队列存储「左子树的节点、右子树的对称节点」,每次取出一对节点检查,再将它们的 “镜像子节点” 成对入队,确保遍历顺序与镜像逻辑一致:

1. 初始化队列:将节点的左子树和右子树作为 “第一对对称节点” 入队;

2. 循环处理队列:

  • 出队:取出队头的两个节点(leftCurr为左子树节点,rightCurr为右子树的对称节点);
  • 检查:按 “边界条件→值” 的顺序判断,若不满足直接返回false
  • 入队:将leftCurr的左、rightCurr的右(第一镜像对),leftCurr的右、rightCurr的左(第二镜像对)依次入队。

3. 队列遍历结束:所有对称节点对均检查通过,返回true

(2)代码实现与详解(需队列辅助)

首先需要实现适配TreeNode*的队列(存储节点指针,每次处理一对节点):

代码语言:javascript
复制
// 队列节点结构(存储二叉树节点指针)
typedef struct QueueNode {
    TreeNode* data;
    struct QueueNode* next;
} QueueNode;

// 队列结构(队头出、队尾入)
typedef struct Queue {
    QueueNode* front;
    QueueNode* rear;
} Queue;

// 队列初始化:空队列,队头队尾均为NULL
void QueueInit(Queue* q) {
    q->front = q->rear = NULL;
}

// 入队:从队尾添加元素
void QueuePush(Queue* q, TreeNode* val) {
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    newNode->data = val;
    newNode->next = NULL;

    if (q->rear == NULL) { // 空队列时,队头队尾指向新节点
        q->front = q->rear = newNode;
    } else { // 非空队列时,链接新节点并更新队尾
        q->rear->next = newNode;
        q->rear = newNode;
    }
}

// 出队:从队头删除元素(需确保队列非空)
void QueuePop(Queue* q) {
    if (q->front == NULL) return;

    QueueNode* temp = q->front;
    q->front = q->front->next;

    if (q->front == NULL) { // 出队后为空,队尾置NULL
        q->rear = NULL;
    }
    free(temp); // 释放原队头内存
}

// 判空:队列是否为空
bool QueueEmpty(Queue* q) {
    return q->front == NULL;
}

// 获取队头元素(需确保队列非空)
TreeNode* QueueFront(Queue* q) {
    return q->front->data;
}

// 销毁队列:释放所有队列节点内存
void QueueDestroy(Queue* q) {
    while (!QueueEmpty(q)) {
        QueuePop(q);
    }
}

再实现核心的 “对称树判断” 逻辑:

代码语言:javascript
复制
bool isSymmetric(TreeNode* root) {
    if (root == NULL) {
        return true; // 兼容空树(题目约束节点数≥1,可省略)
    }

    // 步骤1:初始化队列,入队第一对对称节点(根的左、根的右)
    Queue q;
    QueueInit(&q);
    QueuePush(&q, root->left);
    QueuePush(&q, root->right);

    // 步骤2:循环处理队列中的对称节点对
    while (!QueueEmpty(&q)) {
        // 出队一对节点(leftCurr对应左子树,rightCurr对应右子树的对称位置)
        TreeNode* leftCurr = QueueFront(&q);
        QueuePop(&q);
        TreeNode* rightCurr = QueueFront(&q);
        QueuePop(&q);

        // 步骤3:结构检查(边界条件)
        if (leftCurr == NULL && rightCurr == NULL) {
            continue; // 均为空,无需后续检查,跳过当前轮
        }
        if (leftCurr == NULL || rightCurr == NULL) {
            QueueDestroy(&q); // 销毁队列避免内存泄漏
            return false; // 一存一失,不对称
        }

        // 步骤4:值检查(均非空时对比值)
        if (leftCurr->val != rightCurr->val) {
            QueueDestroy(&q);
            return false;
        }

        // 步骤5:入队下一对镜像节点(关键:确保对称顺序)
        QueuePush(&q, leftCurr->left);   // 左的左
        QueuePush(&q, rightCurr->right); // 右的右(第一镜像对)
        QueuePush(&q, leftCurr->right);  // 左的右
        QueuePush(&q, rightCurr->left);  // 右的左(第二镜像对)
    }

    // 所有对称节点对均检查通过
    QueueDestroy(&q);
    return true;
}
(3)关键细节
  • 入队顺序的重要性:必须严格按 “左左→右右→左右→右左” 入队,确保下一轮处理的是 “对称位置” 的节点。若顺序错误(如左左→左右→右右→右左),会导致对比错位(如左左和左右对比,而非左左和右右);
  • 空节点的处理:即使节点为空,若其对称位置有节点,也需入队检查;
  • 内存安全:所有返回路径(包括false)均需销毁队列,避免队列节点的内存泄漏。

四、另一棵树的子树

题目链接:https://leetcode.cn/problems/subtree-of-another-tree/description/

4.1 题目分析

题目要求:判断二叉树 root 中是否包含与 subRoot 结构完全一致、节点值完全相等的子树(子树定义:包含某个节点及其所有后代节点,root 也可视为自身的子树),是则返回true,否则返回false

约束条件

  • root 节点数范围:[1, 2000]subRoot 节点数范围:[1, 1000](均非空树,无需处理空树边界);
  • 节点值范围:[-10⁴, 10⁴](支持负数对比)。

4.2 核心解题思路

“子树判断” 的本质是 “遍历 + 相同树校验”

  1. 遍历主树 root:逐个访问 root 的每个节点,将每个节点视为 “潜在的子树根节点”;
  2. 校验相同树:对每个 “潜在子树根节点”,调用 “相同树判断函数”(复用“相同的树”逻辑),检查以该节点为根的子树是否与 subRoot 完全相同;
  3. 结果判定:若存在任意一个 “潜在子树根节点” 通过校验,返回true;遍历完 root 所有节点均未通过,返回false

下面同样也介绍两种方法。

4.3 详细解题步骤与代码实现

4.3.1 核心辅助函数:判断两棵树是否相同(复用“相同的树”逻辑)

子树判断依赖 “相同树校验”,需先实现辅助函数 isSameTree,用于判断两棵树是否结构和值完全一致:

代码语言:javascript
复制
// 辅助函数:判断树a和树b是否完全相同(结构+值)
bool isSameTree(TreeNode* a, TreeNode* b) {
    // 边界1:两棵树当前节点均为空 → 相同
    if (a == NULL && b == NULL) {
        return true;
    }
    // 边界2:一棵空、一棵非空 → 不同
    if (a == NULL || b == NULL) {
        return false;
    }
    // 边界3:节点值不同 → 不同
    if (a->val != b->val) {
        return false;
    }
    // 递归校验左子树和右子树(需均相同)
    return isSameTree(a->left, b->left) && isSameTree(a->right, b->right);
}
4.3.2 方法一:递归法
(1)解题步骤

递归遍历 root 的逻辑:

  1. 当前节点校验:判断以 root 当前节点为根的子树是否与 subRoot 相同,若是则返回true
  2. 递归左子树:递归检查 root 的左子树是否包含 subRoot
  3. 递归右子树:递归检查 root 的右子树是否包含 subRoot
  4. 结果合并:左子树或右子树任意一个包含 subRoot,则返回true;否则返回false
(2)代码实现与详解
代码语言:javascript
复制
bool isSubtree(TreeNode* root, TreeNode* subRoot) {
    // 边界:若root遍历到空(无更多节点可校验),返回false
    if (root == NULL) {
        return false;
    }

    // 步骤1:校验当前节点为根的子树是否与subRoot相同
    if (isSameTree(root, subRoot)) {
        return true;
    }

    // 步骤2:递归校验左子树和右子树(只要一个包含就返回true)
    return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}
(3)关键细节
  • 短路优化:若 root 的左子树已找到匹配的子树,|| 运算符会直接返回true,无需再递归右子树,提升效率;
  • 遍历顺序:本质是 root 的前序遍历(先校验当前节点,再遍历左右子树),确保每个节点都被作为 “潜在子树根” 检查。
4.3.3 方法二:迭代法(DFS / 栈实现)
(1)解题步骤

迭代遍历 root 的逻辑如下(用栈实现 DFS,模拟递归遍历顺序):

1. 初始化栈:root 入栈,栈存储 root 中待校验的 “潜在子树根节点”。

2. 循环处理栈:

  • 出栈:取出栈顶节点(当前待校验的节点);
  • 校验:调用 isSameTree 检查该节点为根的子树是否与 subRoot 相同,若是则返回true
  • 入栈:将该节点的右子树、左子树依次入栈(栈是 LIFO,先入右子树确保左子树先被处理,与递归顺序一致)。

3. 栈空返回:遍历完所有节点均未匹配,返回false

(2)代码实现与详解(用栈辅助)

首先实现适配 TreeNode* 的栈结构:

代码语言:javascript
复制
// 栈节点结构(存储二叉树节点指针)
typedef struct StackNode {
    TreeNode* data;
    struct StackNode* next;
} StackNode;

// 栈结构(栈顶入、栈顶出)
typedef struct Stack {
    StackNode* top; // 栈顶指针
} Stack;

// 栈初始化:空栈,栈顶为NULL
void StackInit(Stack* s) {
    s->top = NULL;
}

// 入栈:从栈顶添加元素
void StackPush(Stack* s, TreeNode* val) {
    StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
    newNode->data = val;
    newNode->next = s->top; // 新节点指向原栈顶
    s->top = newNode;       // 更新栈顶为新节点
}

// 出栈:从栈顶删除元素(需确保栈非空)
void StackPop(Stack* s) {
    if (s->top == NULL) return;

    StackNode* temp = s->top;
    s->top = s->top->next; // 栈顶后移
    free(temp);            // 释放原栈顶内存
}

// 判空:栈是否为空
bool StackEmpty(Stack* s) {
    return s->top == NULL;
}

// 获取栈顶元素(需确保栈非空)
TreeNode* StackTop(Stack* s) {
    return s->top->data;
}

// 销毁栈:释放所有栈节点内存
void StackDestroy(Stack* s) {
    while (!StackEmpty(s)) {
        StackPop(s);
    }
}

再来实现核心的“子树判断” 逻辑:

代码语言:javascript
复制
bool isSubtree(TreeNode* root, TreeNode* subRoot) {
    // 步骤1:初始化栈,将root入栈(第一个待校验节点)
    Stack s;
    StackInit(&s);
    StackPush(&s, root);

    // 步骤2:循环处理栈中所有待校验节点
    while (!StackEmpty(&s)) {
        // 出栈:取出当前待校验节点
        TreeNode* curr = StackTop(&s);
        StackPop(&s);

        // 步骤3:校验当前节点为根的子树是否与subRoot相同
        if (isSameTree(curr, subRoot)) {
            StackDestroy(&s); // 销毁栈避免内存泄漏
            return true;
        }

        // 步骤4:将当前节点的右、左子树入栈(确保左子树先处理)
        if (curr->right != NULL) {
            StackPush(&s, curr->right);
        }
        if (curr->left != NULL) {
            StackPush(&s, curr->left);
        }
    }

    // 步骤5:遍历完所有节点均未匹配,销毁栈并返回false
    StackDestroy(&s);
    return false;
}
(3)关键细节
  • 入栈顺序:先入右子树、再入左子树 —— 因栈是 “后进先出”(LIFO),左子树后入栈会先出栈,确保遍历顺序与递归法一致(左子树优先校验);
  • 内存安全:所有返回路径(包括truefalse)均需调用 StackDestroy 释放栈节点内存,避免内存泄漏;
  • 空节点过滤:入栈前判断子树是否为空,避免空节点入栈(空节点不可能与非空的 subRoot 匹配,减少无效校验)。

五、二叉树性质补充

💡二叉树性质 对于任何一棵二叉树,如果度为0其结点个数为

n_0
n_0

,度为2的分支结点个数为

n_2
n_2

,则有

n_0=n_2+1
n_0=n_2+1

上述性质证明如下:

假设一个二叉树有a个度为2的结点,b个度为1的结点,c个叶子结点,则这个二叉树的边数就是

2a+b
2a+b

另一方面,由于一共有

a+b+c
a+b+c

个节点,所以边数又等于

a+b+c-1
a+b+c-1

结合上面两个公式可以得到:

2a+b = a+b+c-1
2a+b = a+b+c-1

,即

a =c-1
a =c-1


总结

本期博客我为大家介绍了四道具有代表性的二叉树OJ题,并从不同角度为大家分析了解题思路。希望本期博客对大家学习二叉树能够有所帮助!感谢大家的支持!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、单值二叉树
    • 1.1 题目分析
    • 1.2 核心解题思路
    • 1.3 详细解题步骤与代码实现
      • 1.3.1 前提:二叉树节点结构定义
      • 1.3.2 方法一:递归遍历
      • 1.3.3 方法二:迭代遍历(BFS / 层序遍历)
  • 二、相同的树
    • 2.1 题目分析
    • 2.2 核心解题思路
    • 2.3 详细解题步骤与代码实现
      • 2.3.1 递归遍历
      • 2.3.2 迭代遍历(BFS / 层序遍历)
  • 三、对称二叉树
    • 3.1 题目分析
    • 3.2 核心解题思路
    • 3.3 详细解题步骤与代码实现
      • 3.3.1 方法一:递归法
      • 3.3.2 方法二:迭代法(BFS / 队列实现)
  • 四、另一棵树的子树
    • 4.1 题目分析
    • 4.2 核心解题思路
    • 4.3 详细解题步骤与代码实现
      • 4.3.1 核心辅助函数:判断两棵树是否相同(复用“相同的树”逻辑)
      • 4.3.2 方法一:递归法
      • 4.3.3 方法二:迭代法(DFS / 栈实现)
  • 五、二叉树性质补充
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档