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

题目链接:https://leetcode.cn/problems/univalued-binary-tree/description/
题目要求:判断一棵二叉树是否为「单值二叉树」—— 即树中所有节点的值都相同,是则返回true,否则返回false。
题目还给出了如下的约束条件:
[1, 100](根节点必存在,无需处理空树边界);[0, 99](整数类型)。单值二叉树的本质是“所有节点的值与根节点的值一致”,因此我们解题的核心思路是:
1. 确定基准值:以根节点的值为target(所有节点需与之相等);以根节点的值为target(所有节点需与之相等);
2. 遍历全树检查:通过「递归遍历」或「迭代遍历」(DFS/BFS)逐个检查节点值,若存在任意节点值≠target,直接返回false;全部检查通过则返回true。
这里我们给出两种主流的遍历方式:
LeetCode 中其实已经为我们预设了节点结构,这里为了清晰展示逻辑,补充定义如下:
// 二叉树节点结构
typedef struct TreeNode {
int val; // 节点值
struct TreeNode *left; // 左子节点
struct TreeNode *right; // 右子节点
} TreeNode; 1. 记录基准值:获取根节点的值target(所有节点需匹配此值);获取根节点的值target(所有节点需匹配此值)。
2. 定义递归检查函数:
NULL(空子树,无节点需检查)→ 返回true;target(找到不匹配节点)→ 返回false;true)。3. 调用递归函数:从根节点开始检查,返回最终结果。
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);
}false,则不会再递归检查右子树(直接返回false),能够大大提升效率。true的原因:空子树是 “合法” 的(没有不匹配节点),例如示例 1 中根节点的右子树有一个null节点,是不影响结果的。 1. 记录基准值:同方法 1,获取根节点值target。
2. 初始化队列:用队列实现 BFS(层序遍历),先将根节点入队。
3. 循环遍历队列:
target,直接返回false; 4. 队列遍历结束:所有节点均匹配,返回true。
// 先实现队列的基础操作(适配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;
}QueueDestroy),避免队列节点的内存泄漏;
题目链接:https://leetcode.cn/problems/same-tree/description/
题目要求:判断两棵二叉树是否「相同」—— 需同时满足两个条件:① 结构完全一致(对应位置的节点存在性相同);② 对应节点的值完全相等,是则返回true,否则返回false。
约束条件:
[0, 100](允许空树,需处理 “两棵都空”“一棵空一棵非空” 的边界);[-10⁴, 10⁴](整数类型,需支持负数对比)。“相同树” 的本质是「两棵树的节点在 “位置” 和 “值” 上完全对齐」,因此核心思路是同步遍历两棵树,每一步对「对应位置的节点」做双重检查:
我们同样也可以分别使用递归和迭代两种遍历方式实现:
递归的核心是 “分而治之”:将 “判断整棵树相同” 拆解为 “判断根节点相同 + 左子树相同 + 右子树相同”,终止条件覆盖所有边界情况:
1. 边界条件处理:
NULL(同时不存在)→ 结构一致,返回true;NULL、另一棵非NULL(一存一失)→ 结构不一致,返回false。 2. 值检查:若两个节点均存在,但值不相等 → 返回false。
3. 递归深入:当前节点检查通过后,递归判断「p 的左子树与 q 的左子树」和「p 的右子树与 q 的右子树」(需两者均相同,故用 “与运算” 连接)。
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);
}NULL节点解引用(p->val),导致程序崩溃;false,则无需再判断右子树,直接返回false(对短路进行的优化)。迭代的核心是 “同步入队、同步出队”:用队列存储两棵树的「对应节点对」,每次取出一对节点做检查,再将它们的左、右子节点成对入队,确保遍历顺序完全同步:
1. 初始化队列:将两棵树的根节点p和q作为 “第一对节点” 入队。
2. 循环处理队列:
currP为 p 的当前节点,currQ为 q 的当前节点);false;currP的左子节点与currQ的左子节点、currP的右子节点与currQ的右子节点成对入队(确保下一轮处理对应位置)。 3. 队列遍历结束:所有节点对均检查通过,返回true。
首先实现适配 “节点对” 的队列(存储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);
}
}再实现核心的 “相同树判断” 逻辑:
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;
}true还是false,都必须调用QueueDestroy释放队列内存,避免队列节点的内存泄漏。continue跳过后续检查(无需入队子节点,因为空节点没有子节点),避免无效操作。
题目链接:https://leetcode.cn/problems/symmetric-tree/description/
题目要求:判断一棵二叉树是否「轴对称」—— 即树的左子树与右子树呈「镜像对称」(结构一致且对称位置的节点值相等),是则返回true,否则返回false。
约束条件:
[1, 1000](根节点必存在,需处理 “左 / 右子树为空” 的边界);[-100, 100](支持负数对比)。“对称二叉树” 的本质不是 “树与自身相同”,而是左子树与右子树镜像。镜像的核心条件是:
下面我同样也用递归法和迭代法两种方法解题。
递归的核心是 “拆分镜像检查”:用辅助函数接收两个节点(左子树的某节点、右子树的对称节点),按以下逻辑判断:
1. 边界条件处理:
NULL(对称位置都空)→ 结构对称,返回true;NULL、另一个非NULL(对称位置一存一失)→ 结构不对称,返回false。 2. 值检查:若两个节点均存在,但值不相等 → 不对称,返回false。
3. 递归深入:当前节点检查通过后,递归检查「左节点的左 vs 右节点的右」和「左节点的右 vs 右节点的左」(需两者均对称,用 “与运算” 连接)。
// 辅助函数:检查两个节点是否镜像对称
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);
}迭代的核心是 “同步处理对称节点对”:用队列存储「左子树的节点、右子树的对称节点」,每次取出一对节点检查,再将它们的 “镜像子节点” 成对入队,确保遍历顺序与镜像逻辑一致:
1. 初始化队列:将节点的左子树和右子树作为 “第一对对称节点” 入队;
2. 循环处理队列:
leftCurr为左子树节点,rightCurr为右子树的对称节点);false;leftCurr的左、rightCurr的右(第一镜像对),leftCurr的右、rightCurr的左(第二镜像对)依次入队。 3. 队列遍历结束:所有对称节点对均检查通过,返回true。
首先需要实现适配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);
}
}再实现核心的 “对称树判断” 逻辑:
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;
}false)均需销毁队列,避免队列节点的内存泄漏。
题目链接:https://leetcode.cn/problems/subtree-of-another-tree/description/
题目要求:判断二叉树 root 中是否包含与 subRoot 结构完全一致、节点值完全相等的子树(子树定义:包含某个节点及其所有后代节点,root 也可视为自身的子树),是则返回true,否则返回false。
约束条件:
root 节点数范围:[1, 2000],subRoot 节点数范围:[1, 1000](均非空树,无需处理空树边界);[-10⁴, 10⁴](支持负数对比)。“子树判断” 的本质是 “遍历 + 相同树校验” :
root:逐个访问 root 的每个节点,将每个节点视为 “潜在的子树根节点”;subRoot 完全相同;true;遍历完 root 所有节点均未通过,返回false。下面同样也介绍两种方法。
子树判断依赖 “相同树校验”,需先实现辅助函数 isSameTree,用于判断两棵树是否结构和值完全一致:
// 辅助函数:判断树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);
} 递归遍历 root 的逻辑:
root 当前节点为根的子树是否与 subRoot 相同,若是则返回true;root 的左子树是否包含 subRoot;root 的右子树是否包含 subRoot;subRoot,则返回true;否则返回false。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);
}root 的左子树已找到匹配的子树,|| 运算符会直接返回true,无需再递归右子树,提升效率;root 的前序遍历(先校验当前节点,再遍历左右子树),确保每个节点都被作为 “潜在子树根” 检查。 迭代遍历 root 的逻辑如下(用栈实现 DFS,模拟递归遍历顺序):
1. 初始化栈:将 root 入栈,栈存储 root 中待校验的 “潜在子树根节点”。
2. 循环处理栈:
isSameTree 检查该节点为根的子树是否与 subRoot 相同,若是则返回true; 3. 栈空返回:遍历完所有节点均未匹配,返回false。
首先实现适配 TreeNode* 的栈结构:
// 栈节点结构(存储二叉树节点指针)
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);
}
}再来实现核心的“子树判断” 逻辑:
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;
}true和false)均需调用 StackDestroy 释放栈节点内存,避免内存泄漏;subRoot 匹配,减少无效校验)。💡二叉树性质 对于任何一棵二叉树,如果度为0其结点个数为

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

,则有

。

上述性质证明如下:
假设一个二叉树有a个度为2的结点,b个度为1的结点,c个叶子结点,则这个二叉树的边数就是

;
另一方面,由于一共有

个节点,所以边数又等于

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

,即

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