数据结构之二叉树算法题

1.查找二叉树第k层节点个数

前情提要:

c 复制代码
typedef int BTDataType;
typedef struct bynode
{
	struct bynode* left;
	struct bynode* right;
	BTDataType data;
}BTNode;
c 复制代码
int TreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	return TreeLevelKSize(root->left, k - 1) + TreeLevelKSize(root->right, k - 1);
}

函数功能:

给定一个二叉树的根节点 root 和一个整数 k,返回该二叉树第 k 层的节点个数。规定根节点为第1层。

递归思路:

  • 如果当前节点为空,则不可能有任何节点,返回0。

  • 如果 k == 1,说明当前层就是我们要找的层,当前节点本身算一个,返回1。

  • 否则,问题转化为:求左子树的第 k-1 层节点个数 + 右子树的第 k-1 层节点个数。

这样通过递归,每次下降一层,直到到达目标层。

如果看不明白,我们还可以先来画图分析一波。

假如查找k==3的这个位置节点个数

  1. k==3,1->2
  2. k==2,2->3
  3. k==1,return 1,3->2
  4. 2->NULL,return 0,计算2的左右返回值为1+0,所以return 1 给节点1
  5. 2->1,此时计算出1的左节点为1,同理计算1的右子树
  6. ...

关键点理解

  • 递归的深度:每次递归调用,k减1,直到k=1时返回当前节点计数。这实际上就是在向下走k-1层。

  • 空节点处理:遇到空节点直接返回0,表示该分支没有节点。

  • 分治思想:将问题分解为左右子树的子问题,最后合并结果。

2.二叉树查找值为x的节点

c 复制代码
BTNode* TreeFind(BTNode* root, BTDataType x)

那有了前面的讲解,此时是不是感觉这题就比较简单呀,查找值为x的节点,这还不简单?

如同前面一样,当root==NULL时,返回NULL,当找到这个相等的值时,直接返回root,最后递归遍历是不是就好了呀!是不是感觉太简单了?

但事实真的是如此吗?我们来写一下尝试一下:

c 复制代码
BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	TreeFind(root->left, x);
	TreeFind(root->right, x);

}

我们看,这是不是就写完了呀?但事实真的就是这样吗?

好了,在这里我们就不再卖关子了,其实这段代码是一个非常经典的错误,这个错误是在哪里呢?我们来看一看:

我们来想一想,在递归中,当if (root->data == x) return root;找到了时,这个root返回到哪里去了,是直接返回出去,还是返回到上一个递归的函数呢?
我想大家都知道这个答案了吧,而且还有第二个问题,我们如果两个if语句都没进去,那这里是不是就没有返回值了呀,而这个函数要求返回BTNode* ,那这里是不是就出现大问题了呀,好了,我们来整理一下上述可能发生的问题:

缺少返回值:

  • 在递归调用 TreeFind(root->left, x); 和 TreeFind(root->right, x); 之后,函数没有返回任何值。这导致当程序执行到函数末尾时,没有 return 语句,行为未定义。在C语言中,非 void 函数必须返回一个值,否则可能返回随机值或导致程序崩溃。

递归结果被丢弃:

  • 即使左子树或右子树中找到了节点,递归调用的返回值没有被捕获和使用。因此,即使找到了节点,函数也会继续执行后续代码,最终可能返回一个未定义的值。

逻辑不完整

  • 正确的查找逻辑应该是:先检查当前节点,若找到则返回;否则在左子树中查找,如果左子树返回非空则返回该结果;否则在右子树中查找,并返回右子树的结果;若左右子树都没找到,则返回 NULL。

那正确写法是不是应该这样呀:

c 复制代码
BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	BTNode* ret1 = TreeFind(root->left, x);
	if (ret1)
		return ret1;

	BTNode* ret2 = TreeFind(root->right, x);
	if (ret2)
		return ret2;

	return NULL;
}

总结:

务必记住:在递归函数中,每次递归调用的返回值都需要被正确处理。要么直接返回,要么根据结果决定下一步。永远不要忽略递归调用的返回值,除非函数是 void 类型。此外,确保函数的所有分支都有明确的 return 语句。

3.单值二叉树

https://leetcode.cn/problems/univalued-binary-tree/description/

c 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
bool isUnivalTree(struct TreeNode* root) {
    if (root == NULL)
    return true;     
    if(root->left && root->left->val!=root->val)
    return false;
    if(root->right && root->right->val!=root->val)
    return false;

    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

解题思路:

我们可以用递归来检查:

  • 如果当前节点为空,则它不影响结果,返回 true。

  • 如果当前节点有左孩子,并且左孩子的值与当前节点值不同,则直接返回 false。

  • 如果当前节点有右孩子,并且右孩子的值与当前节点值不同,也返回 false。

  • 如果当前节点与左右孩子值都相等(或没有孩子),则递归检查左子树和右子树是否也是单值二叉树。

  • 只有左右子树都返回 true,整棵树才是单值。

4.相同的树

https://leetcode.cn/problems/same-tree/description/

c 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p == NULL && q == NULL)
    return true;
    if(p == NULL || q == NULL)
    return false;
    
    if(p->val != q->val)
    return false;

    return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);

}

递归思路:

  • 如果两个节点都为空,则它们相同。

  • 如果其中一个为空而另一个不为空,则结构不同,直接返回 false。

  • 如果两个都不为空,则比较它们的值,若值不同则返回 false。

  • 若值相同,则递归比较它们的左子树和右子树是否分别相同。只有左右子树都相同时,整棵树才相同。

5.对称二叉树

https://leetcode.cn/problems/symmetric-tree/description/

c 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
 bool isMirror(struct TreeNode* p, struct TreeNode* q)
 {
    if(p == NULL && q == NULL)
    return true;
    if(p == NULL || q == NULL)
    return false;
    return (p->val == q->val) 
    && isMirror(p->left, q->right) 
    && isMirror(p->right, q->left);
 }
bool isSymmetric(struct TreeNode* root) {
    if(root == NULL)
    return true;
    return isMirror(root->left,root->right);
}

解题思路:

一棵树对称意味着:

  • 根节点的左子树和右子树互为镜像。

两棵子树互为镜像的条件是:

  • 它们的根节点值相等。

  • 一棵树的左子树与另一棵树的右子树镜像对称。

  • 一棵树的右子树与另一棵树的左子树镜像对称。

这形成了递归定义。

6.另一颗树的子树

https://leetcode.cn/problems/subtree-of-another-tree/submissions/708473483/

c 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
 bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p == NULL && q == NULL)
    return true;
    if(p == NULL || q == NULL)
    return false;
    
    if(p->val != q->val)
    return false;
    return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);

}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot) {
    if(root == NULL)
    return false;
    if(subRoot == NULL)
    return true;
    // 检查当前节点为根的树是否与 subRoot 相同
    if (isSameTree(root, subRoot))
    return true;
    // 否则递归检查左子树和右子树
    return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);

}

解题思路:

我们需要遍历 root 的每个节点,检查以该节点为根的子树是否与 subRoot 完全相同。这可以用递归实现:

  1. 先编写一个辅助函数 isSameTree(p, q),判断两棵树是否完全相同
  2. 在主函数 isSubtree 中:
  • 如果 root 为空,则不可能包含非空子树,返回 false。

  • 如果 subRoot 为空,根据定义,空树是任何树的子树,返回 true(但通常题目中 subRoot 非空,这里为了严谨可加)。

  • 否则,先检查当前 root 和 subRoot 是否相同,若相同则返回 true。

  • 如果不同,则递归检查 root 的左子树或右子树是否包含 subRoot,只要一边找到就返回 true。

7.二叉树的前序遍历

https://leetcode.cn/problems/binary-tree-preorder-traversal/description/

c 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
 int treeSize(struct TreeNode* root) {
    if (root == NULL) 
    return 0;
    return treeSize(root->left) + treeSize(root->right) + 1;
}
void preorder(struct TreeNode* root,int* a,int* i)
{
    if(root == NULL)
    {
        return;
    }
    a[(*i)++] = root->val;
    preorder(root->left,a,i);
    preorder(root->right,a,i);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    *returnSize = treeSize(root);
    int* a=(int*)malloc(sizeof(int)*(*returnSize));
    int i = 0;
    preorder(root,a,&i);
    return a;

}

看似代码不起眼,实则在这里稍不留神,就会有坑哦。

首先,题目要求有malloc开辟节点空间,那么我们是不是要知道开辟多少个呀,所以再用一次我们的treeSize函数来求一下节点个数,开辟出相应的空间。

然后接下来会有一个大坑,为什么我们这里要&i呀,直接传i不行吗???

如果你直接传,那你就掉入陷阱里了,我们在递归调用函数时,如果传i而不是&i,那么i就会一直被重置而不会真正的改变数值,所以在这里我们需要去传指针。

8.二叉树前序遍历

https://www.nowcoder.com/practice/4b91205483694f449f94c179883c1fef

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

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}

	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}

BTNode* CreateTree(char* n,int* i)
{
    if(n[*i] =='#')
    {
        (*i)++;
        return NULL;
    }
    BTNode* root = (BTNode*)malloc(sizeof(BTNode));
    root->data = n[(*i)++];
    root->left = CreateTree(n, i);
    root->right = CreateTree(n, i);
    return root;
}

int main() {
    char n[100];
    scanf("%s",n);
    int i = 0;
    BTNode* root = CreateTree(n,&i);
    InOrder(root);
    return 0;
}

代码逻辑与思路:

中序遍历函数 InOrder:

  • 递归实现:先遍历左子树,然后打印节点值(使用 %c 格式,所以输出字符),再遍历右子树。

  • 如果节点为空,直接返回。

构建二叉树函数 CreateTree:

  • 参数:字符串指针 n,以及一个整型指针 i(用于在递归中共享当前处理的位置)。

  • 首先判断当前字符 n[*i] 是否为 '#':

  • 如果是,则 (*i)++ 跳过这个 #,并返回 NULL 表示空节点。

否则,创建一个新节点:

  • 分配内存。

  • 将当前字符赋值给节点数据,并且 (*i)++ 后移。

  • 递归构建左子树,传入相同的 i。

  • 递归构建右子树,传入相同的 i。

  • 返回根节点。

主函数:

  • 定义字符数组 n 存储输入字符串。

  • 用 scanf("%s", n) 读取字符串(注意:遇到空格会停止,但输入中可能没有空格,只有字符和#)。

  • 初始化索引 i = 0。

  • 调用 CreateTree 构建树,传入 &i 以便在递归中更新索引。

  • 然后调用 InOrder 进行中序遍历并输出。

易错点与注意事项:

  1. 索引必须传指针
  • 在递归构建树时,所有递归调用必须共享同一个索引变量。如果直接传 i(值传递),每一层递归都会拥有独立的副本,导致索引无法正确更新。阱。
  1. 处理 '#' 的顺序
  • 先判断当前字符是否为 '#',再 (*i)++。如果先自增再判断,就会跳过当前字符而误判。
相关推荐
qianbo_insist2 小时前
鱼眼图像的三维投影逆变换和AI计算
人工智能·opencv·算法
草莓熊Lotso2 小时前
Linux 进程间通信之 System V 共享内存:IPC 的原理与实战
linux·运维·服务器·c语言·数据库·c++·人工智能
我怎么又饿了呀2 小时前
DataWhale—大模型的算法基础(文本表示与词向量)
算法
尽兴-2 小时前
从零到精通:Redis 7 核心数据结构实战与单机部署指南
数据结构·数据库·redis·部署·redis7
珠海西格电力2 小时前
5G+物联网,零碳园区管理系统的“信息高速路”
大数据·人工智能·物联网·算法·5g
豆浆煮粉2 小时前
Linux驱动开发理解指针与结构体
linux·c语言·驱动开发
疋瓞2 小时前
C\C++\python对比_概览(1)
c语言·c++·python
见叶之秋2 小时前
【数据结构】详解双向链表
数据结构·链表
海盗猫鸥2 小时前
「Linux工具」gcc/g++
linux·c语言·c++