90%考生倒下的二叉树高阶考点全景解剖之:面试黑暗森林法则:如何在LCA/序列化/重建的递归坟场生存? 二叉树高阶手术刀:二刷4w行后,我拆解了LCA/序列化/重建的死亡陷阱 降维打击

引子:

 

这次,二刷热题101过程中,意见到了看山不是山看水不是水的程度了,现在应该做i的反而是:系统根据每类题型科学总结,对每一道背后的思路系统分析:

出了这些之外,树作为堆栈队列代码段一个非常重要的基础数据结构,注定了这个知识点一定是一个面试高频考点!!!

1 基础模块:

链表、树、栈队列 、查找排序算法

树:

2 进阶模块:

递归回溯:

动态规划:

贪心算法:

字符串双指针:

1 c语言二叉树精讲(上):基础遍历与常见操作

二叉树作为最重要的数据结构之一,在各大公司的面试中占据着举足轻重的地位。它不仅考察你对数据结构本身的理解,更是对递归、迭代、队列、栈等基础算法能力的综合检验。本文将结合牛客网上的经典二叉树题目,通过分析实际代码案例,深入剖析二叉树的常见操作、易错点以及优化策略,帮助你系统掌握二叉树的精髓,轻松应对面试挑战。

1. 结构体定义与预备知识

在C语言中,二叉树节点通常定义如下:

/**
 * struct TreeNode {
 * int val;
 * struct TreeNode *left;
 * struct TreeNode *right;
 * };
 */

这是一个标准的二叉树节点定义,包含一个整型值 val,以及指向左右子节点的指针 leftright。理解这种递归定义是理解二叉树一切操作的基础。

2. 二叉树的遍历

二叉树的遍历是其最基本也是最重要的操作。主要有三种深度优先遍历方式:前序遍历、中序遍历和后序遍历。它们的核心区别在于访问根节点的时机。

2.1 BM23 二叉树的前序遍历

题目要求: 给定一个二叉树的根节点 root,返回其节点值的前序遍历。

前序遍历定义: 访问根节点 -> 遍历左子树 -> 遍历右子树。

你的代码分析:

// BM23二叉树的前序遍历.c
void doFunc(struct TreeNode *root, int *res, int *returnSize)
{
    if(root==NULL){
        return ;
    }
    res[(*returnSize)++] = root->val;
    doFunc(root->left,res,returnSize);
    doFunc(root->right,res,returnSize);
    return ;
}

int *preorderTraversal(struct TreeNode *root, int *returnSize)
{
    int* res  = (int* )malloc(1000*sizeof(int)); // 问题1:硬编码数组大小
    *returnSize =0  ;
    doFunc(root,res,returnSize);
    return res;
}

你的代码优点:

  • 递归实现清晰: doFunc 函数完美地实现了前序遍历的递归逻辑,思路非常清晰。

  • 指针传递 returnSize 使用 int *returnSize 来动态记录结果数组的实际大小是正确的做法,使得调用者可以知道返回数组的有效元素数量。

你的代码存在的问题与改进:

  1. 硬编码数组大小(常见误区):

    • 问题: malloc(1000 * sizeof(int)) 硬编码了结果数组的最大容量为1000。这在实际面试或项目中是非常危险的。如果二叉树的节点数超过1000,程序就会发生越界访问,导致崩溃。如果节点数远小于1000,又会造成内存浪费。

    • 误区: 很多初学者在遇到需要返回动态数组的题目时,会想当然地给一个“足够大”的固定大小。这种做法虽然在一些测试用例下可能通过,但在实际应用中是不可取的。

    • 如何避免:

      • 两次遍历法: 第一次遍历计算二叉树的节点总数,然后根据节点总数精确分配内存。第二次遍历填充结果。

      • 动态扩容法: 初始分配一个较小的内存块,当需要存储更多元素时,使用 realloc 进行扩容。这是更通用的方法。

  2. doFunc 函数名不够语义化:

    • 问题: doFunc 这样的命名太泛泛,无法从函数名中直接看出其功能。

    • 改进: 建议改为 preorderRecursivetraverseAndCollect 等更能体现其功能的名字。

改进后的代码(动态扩容法):

为了避免硬编码数组大小的问题,我们可以采用动态扩容的方法。这种方法在遇到数组空间不足时,会自动增加数组的容量。

#include <stdlib.h> // For malloc, realloc, free

// 假设TreeNode结构体已在其他地方定义或在函数前定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

/**
 * @brief 辅助函数:递归进行前序遍历并收集节点值
 *
 * @param root 当前遍历到的节点
 * @param res 指向结果数组指针的指针,用于动态扩容
 * @param capacity 指向当前结果数组容量的指针
 * @param returnSize 指向已收集节点数量的指针
 */
void preorderCollect(struct TreeNode *root, int **res, int *capacity, int *returnSize) {
    // 递归终止条件:节点为空
    if (root == NULL) {
        return;
    }

    // 检查容量是否足够,如果不足则进行扩容
    if (*returnSize >= *capacity) {
        // 如果容量为0,则初始化一个默认容量,避免realloc(NULL, 0)的未定义行为
        *capacity = (*capacity == 0) ? 10 : (*capacity * 2); // 常用扩容策略:翻倍
        int *new_res = (int *)realloc(*res, *capacity * sizeof(int));
        if (new_res == NULL) {
            // 内存分配失败处理,实际应用中可能需要更复杂的错误报告
            fprintf(stderr, "Memory reallocation failed in preorderCollect.\n");
            exit(EXIT_FAILURE); // 直接退出程序或返回错误码
        }
        *res = new_res;
    }

    // 访问根节点
    (*res)[(*returnSize)++] = root->val;

    // 递归遍历左子树
    preorderCollect(root->left, res, capacity, returnSize);
    // 递归遍历右子树
    preorderCollect(root->right, res, capacity, returnSize);
}

/**
 * @brief 二叉树的前序遍历主函数
 *
 * @param root TreeNode类,二叉树的根节点
 * @param returnSize 返回数组的实际元素个数
 * @return int* 返回一个包含前序遍历结果的整型一维数组
 */
int *preorderTraversal(struct TreeNode *root, int *returnSize) {
    int *res = NULL;     // 初始化结果数组为NULL
    int capacity = 0;    // 初始化容量为0
    *returnSize = 0;     // 初始化实际元素个数为0

    // 调用辅助函数进行遍历和收集
    preorderCollect(root, &res, &capacity, returnSize);

    // 优化:如果实际使用的空间远小于分配的空间,可以再次realloc缩小
    // 但通常在面试中,只要解决了越界问题即可,这步可以省略
    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) { // 只有在需要空间但realloc失败时才报错
            fprintf(stderr, "Final memory reallocation failed in preorderTraversal.\n");
            // 此时res仍然指向旧的内存块,可以返回res或者根据策略处理
        } else if (*returnSize == 0) { // 如果没有元素,释放可能存在的空数组
            free(res);
            res = NULL;
        }
        else {
            res = final_res;
        }
    }

    return res;
}

非递归实现(迭代法):

除了递归,前序遍历也可以使用栈来实现非递归版本。这对于某些场景(如避免栈溢出)是很有用的。

#include <stdlib.h>

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

// 定义一个简单的栈结构,用于存储TreeNode指针
typedef struct {
    struct TreeNode **array;
    int top;
    int capacity;
} Stack;

// 初始化栈
Stack* createStack(int capacity) {
    Stack *s = (Stack *)malloc(sizeof(Stack));
    if (s == NULL) return NULL;
    s->capacity = capacity;
    s->array = (struct TreeNode **)malloc(s->capacity * sizeof(struct TreeNode *));
    if (s->array == NULL) {
        free(s);
        return NULL;
    }
    s->top = -1;
    return s;
}

// 检查栈是否为空
bool isStackEmpty(Stack *s) {
    return s->top == -1;
}

// 检查栈是否已满
bool isStackFull(Stack *s) {
    return s->top == s->capacity - 1;
}

// 扩容栈
bool expandStack(Stack *s) {
    int new_capacity = s->capacity * 2;
    if (new_capacity == 0) new_capacity = 1; // 避免0容量
    struct TreeNode **new_array = (struct TreeNode **)realloc(s->array, new_capacity * sizeof(struct TreeNode *));
    if (new_array == NULL) return false; // 扩容失败
    s->array = new_array;
    s->capacity = new_capacity;
    return true;
}

// 压入元素
void pushStack(Stack *s, struct TreeNode *node) {
    if (isStackFull(s)) {
        if (!expandStack(s)) {
            fprintf(stderr, "Stack expansion failed!\n");
            exit(EXIT_FAILURE);
        }
    }
    s->array[++s->top] = node;
}

// 弹出元素
struct TreeNode* popStack(Stack *s) {
    if (isStackEmpty(s)) return NULL;
    return s->array[s->top--];
}

// 销毁栈
void destroyStack(Stack *s) {
    if (s == NULL) return;
    free(s->array);
    free(s);
}

/**
 * @brief 二叉树的前序遍历 (迭代法)
 *
 * @param root TreeNode类,二叉树的根节点
 * @param returnSize 返回数组的实际元素个数
 * @return int* 返回一个包含前序遍历结果的整型一维数组
 */
int *preorderTraversal_Iterative(struct TreeNode *root, int *returnSize) {
    int *res = NULL;
    int capacity = 0;
    *returnSize = 0;

    if (root == NULL) {
        return NULL;
    }

    Stack *s = createStack(10); // 初始化栈,给定一个初始容量
    if (s == NULL) {
        fprintf(stderr, "Failed to create stack.\n");
        return NULL;
    }

    pushStack(s, root); // 将根节点压入栈

    while (!isStackEmpty(s)) {
        struct TreeNode *current = popStack(s);

        // 访问当前节点
        if (*returnSize >= capacity) {
            capacity = (capacity == 0) ? 10 : (capacity * 2);
            int *new_res = (int *)realloc(res, capacity * sizeof(int));
            if (new_res == NULL) {
                fprintf(stderr, "Memory reallocation failed in preorderTraversal_Iterative.\n");
                destroyStack(s);
                free(res); // 释放已分配的部分结果数组
                exit(EXIT_FAILURE);
            }
            res = new_res;
        }
        res[(*returnSize)++] = current->val;

        // 先右子节点入栈,后左子节点入栈,确保左子节点先被弹出
        if (current->right != NULL) {
            pushStack(s, current->right);
        }
        if (current->left != NULL) {
            pushStack(s, current->left);
        }
    }

    destroyStack(s); // 销毁栈

    // 最终调整结果数组大小
    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) {
            fprintf(stderr, "Final memory reallocation failed in preorderTraversal_Iterative.\n");
        } else if (*returnSize == 0) {
            free(res);
            res = NULL;
        } else {
            res = final_res;
        }
    }

    return res;
}

2.2 BM2 二叉树的中序遍历 (略)

题目要求: 给定一个二叉树的根节点 root,返回其节点值的中序遍历。

中序遍历定义: 遍历左子树 -> 访问根节点 -> 遍历右子树。

由于你没有提供中序遍历的代码,这里直接给出通用的递归和迭代实现。

递归实现:

#include <stdlib.h>

struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

void inorderCollect(struct TreeNode *root, int **res, int *capacity, int *returnSize) {
    if (root == NULL) {
        return;
    }

    inorderCollect(root->left, res, capacity, returnSize); // 遍历左子树

    // 访问根节点
    if (*returnSize >= *capacity) {
        *capacity = (*capacity == 0) ? 10 : (*capacity * 2);
        int *new_res = (int *)realloc(*res, *capacity * sizeof(int));
        if (new_res == NULL) {
            fprintf(stderr, "Memory reallocation failed in inorderCollect.\n");
            exit(EXIT_FAILURE);
        }
        *res = new_res;
    }
    (*res)[(*returnSize)++] = root->val;

    inorderCollect(root->right, res, capacity, returnSize); // 遍历右子树
}

int *inorderTraversal(struct TreeNode *root, int *returnSize) {
    int *res = NULL;
    int capacity = 0;
    *returnSize = 0;

    inorderCollect(root, &res, &capacity, returnSize);

    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) {
             fprintf(stderr, "Final memory reallocation failed in inorderTraversal.\n");
        } else if (*returnSize == 0) {
            free(res);
            res = NULL;
        } else {
            res = final_res;
        }
    }
    return res;
}

迭代实现:

#include <stdlib.h>
#include <stdbool.h> // For bool type

// 假设TreeNode结构体和Stack结构体及相关函数已在其他地方定义或在此之前定义

int *inorderTraversal_Iterative(struct TreeNode *root, int *returnSize) {
    int *res = NULL;
    int capacity = 0;
    *returnSize = 0;

    Stack *s = createStack(10);
    if (s == NULL) {
        fprintf(stderr, "Failed to create stack for inorder traversal.\n");
        return NULL;
    }

    struct TreeNode *current = root;
    while (current != NULL || !isStackEmpty(s)) {
        // 一直向左,并将沿途节点入栈
        while (current != NULL) {
            pushStack(s, current);
            current = current->left;
        }

        // 左子树访问完毕,弹出栈顶节点(根节点),并访问
        current = popStack(s);
        if (current != NULL) {
            if (*returnSize >= capacity) {
                capacity = (capacity == 0) ? 10 : (capacity * 2);
                int *new_res = (int *)realloc(res, capacity * sizeof(int));
                if (new_res == NULL) {
                    fprintf(stderr, "Memory reallocation failed in inorderTraversal_Iterative.\n");
                    destroyStack(s);
                    free(res);
                    exit(EXIT_FAILURE);
                }
                res = new_res;
            }
            res[(*returnSize)++] = current->val;
            // 转向右子树
            current = current->right;
        }
    }
    destroyStack(s);
    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) {
             fprintf(stderr, "Final memory reallocation failed in inorderTraversal_Iterative.\n");
        } else if (*returnSize == 0) {
            free(res);
            res = NULL;
        } else {
            res = final_res;
        }
    }
    return res;
}

2.3 BM3 二叉树的后序遍历 (略)

题目要求: 给定一个二叉树的根节点 root,返回其节点值的后序遍历。

后序遍历定义: 遍历左子树 -> 遍历右子树 -> 访问根节点。

同样,没有你的代码,直接给出通用实现。

递归实现:

#include <stdlib.h>

struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

void postorderCollect(struct TreeNode *root, int **res, int *capacity, int *returnSize) {
    if (root == NULL) {
        return;
    }

    postorderCollect(root->left, res, capacity, returnSize);  // 遍历左子树
    postorderCollect(root->right, res, capacity, returnSize); // 遍历右子树

    // 访问根节点
    if (*returnSize >= *capacity) {
        *capacity = (*capacity == 0) ? 10 : (*capacity * 2);
        int *new_res = (int *)realloc(*res, *capacity * sizeof(int));
        if (new_res == NULL) {
            fprintf(stderr, "Memory reallocation failed in postorderCollect.\n");
            exit(EXIT_FAILURE);
        }
        *res = new_res;
    }
    (*res)[(*returnSize)++] = root->val;
}

int *postorderTraversal(struct TreeNode *root, int *returnSize) {
    int *res = NULL;
    int capacity = 0;
    *returnSize = 0;

    postorderCollect(root, &res, &capacity, returnSize);

    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) {
             fprintf(stderr, "Final memory reallocation failed in postorderTraversal.\n");
        } else if (*returnSize == 0) {
            free(res);
            res = NULL;
        } else {
            res = final_res;
        }
    }
    return res;
}

迭代实现(双栈法):

后序遍历的迭代实现相对复杂一些,通常需要两个栈。

#include <stdlib.h>
#include <stdbool.h>

// 假设TreeNode结构体和Stack结构体及相关函数已在其他地方定义或在此之前定义

int *postorderTraversal_Iterative(struct TreeNode *root, int *returnSize) {
    int *res = NULL;
    int capacity = 0;
    *returnSize = 0;

    if (root == NULL) {
        return NULL;
    }

    Stack *s1 = createStack(10); // 主栈
    Stack *s2 = createStack(10); // 辅助栈,用于反转结果
    if (s1 == NULL || s2 == NULL) {
        fprintf(stderr, "Failed to create stacks for postorder traversal.\n");
        if (s1) destroyStack(s1);
        if (s2) destroyStack(s2);
        return NULL;
    }

    pushStack(s1, root);

    // 将节点按照 "根 -> 右 -> 左" 的顺序压入辅助栈s2
    while (!isStackEmpty(s1)) {
        struct TreeNode *current = popStack(s1);
        pushStack(s2, current);

        if (current->left != NULL) {
            pushStack(s1, current->left);
        }
        if (current->right != NULL) {
            pushStack(s1, current->right);
        }
    }

    // 从辅助栈s2中依次弹出,即为 "左 -> 右 -> 根" 的后序遍历顺序
    while (!isStackEmpty(s2)) {
        struct TreeNode *current = popStack(s2);
        if (*returnSize >= capacity) {
            capacity = (capacity == 0) ? 10 : (capacity * 2);
            int *new_res = (int *)realloc(res, capacity * sizeof(int));
            if (new_res == NULL) {
                fprintf(stderr, "Memory reallocation failed in postorderTraversal_Iterative.\n");
                destroyStack(s1);
                destroyStack(s2);
                free(res);
                exit(EXIT_FAILURE);
            }
            res = new_res;
        }
        res[(*returnSize)++] = current->val;
    }

    destroyStack(s1);
    destroyStack(s2);

    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) {
             fprintf(stderr, "Final memory reallocation failed in postorderTraversal_Iterative.\n");
        } else if (*returnSize == 0) {
            free(res);
            res = NULL;
        } else {
            res = final_res;
        }
    }
    return res;
}

总结: 无论是前序、中序还是后序遍历,递归实现通常更简洁直观,但可能面临栈溢出的风险(对于非常深的树)。迭代实现虽然代码量稍大,但可以避免栈溢出,并且对于某些场景(如层次遍历)是更自然的。在面试中,两种实现方式都应该掌握。

3. 二叉树的深度

二叉树的深度(或高度)是指从根节点到最远叶节点的最长路径上的节点数。

3.1 BM28 二叉树的最大深度

题目要求: 给定一个二叉树的根节点 root,返回其最大深度。

最大深度定义:

  • 空树的深度为0。

  • 非空树的深度为 max(左子树深度, 右子树深度) + 1

你的代码分析:

// BM28二叉树最大深度.c
int maxDepth(struct TreeNode *root)
{
    if (!root) return 0 ; // 递归终止条件
    int lLen = maxDepth(root->left); // 递归计算左子树深度
    int rLen = maxDepth(root->right); // 递归计算右子树深度
    return lLen>rLen?lLen+1:rLen+1; // 返回左右子树深度的较大值加1
}

你的代码优点:

  • 简洁高效: 你的 maxDepth 函数非常精炼且完全正确地实现了二叉树最大深度的计算。这是一个典型的递归解法,体现了分治的思想。

  • 边界条件处理得当: if (!root) return 0; 很好地处理了空树的深度为0的边界情况。

你的代码存在的问题与改进:

几乎没有问题,这是一个非常标准的优秀答案。

可以改进的地方(风格和可读性):

  1. 三元运算符与 fmax 虽然你的三元运算符 lLen>rLen?lLen+1:rLen+1 完全正确,但使用 fmax(需要 #include <math.h>)或者简单的 if-else 结构可能会让代码在某些情况下更具可读性,尤其是在表达式更复杂时。但对于这种简单的比较,三元运算符也很常见。

    #include <math.h> // For fmax
    
    int maxDepth(struct TreeNode *root)
    {
        if (root == NULL) {
            return 0;
        }
        int leftDepth = maxDepth(root->left);
        int rightDepth = maxDepth(root->right);
        return (int)fmax(leftDepth, rightDepth) + 1; // 显式类型转换
    }
    
    

    或者

    int maxDepth(struct TreeNode *root)
    {
        if (root == NULL) {
            return 0;
        }
        int leftDepth = maxDepth(root->left);
        int rightDepth = maxDepth(root->right);
        if (leftDepth > rightDepth) {
            return leftDepth + 1;
        } else {
            return rightDepth + 1;
        }
    }
    
    

总结: 最大深度问题是二叉树递归思想的入门题,你的解法非常标准。理解其递归定义和边界条件是关键。

4. 合并二叉树

合并二叉树通常指将两棵二叉树合并成一棵新的二叉树,合并规则可能多种多样。

4.1 BM32 合并二叉树

题目要求: 给定两棵二叉树 t1t2,将它们合并成一个新的二叉树。合并的规则是:如果两个节点重叠,那么将它们的值相加作为新树的节点值;否则,不重叠的节点将直接作为新树的节点。

你的代码分析:

// BM32合并二叉树.c
struct TreeNode *mergeTrees(struct TreeNode *t1, struct TreeNode *t2)
{
    if (t1 == NULL)
        return t2; // 如果t1为空,直接返回t2(及其后续子树)
    if (t2 == NULL)
        return t1; // 如果t2为空,直接返回t1(及其后续子树)

    t1->val +=t2->val; // 问题1:直接修改了t1的节点值,可能不符合题意“生成新树”
    t1->left = mergeTrees(t1->left,t2->left); // 递归合并左子树
    t1->right = mergeTrees(t1->right,t2->right); // 递归合并右子树
    return t1;
}

你的代码优点:

  • 递归逻辑正确: 核心的递归合并逻辑是正确的,即对左右子树进行递归合并。

  • 边界条件处理得当: if (t1 == NULL) return t2;if (t2 == NULL) return t1; 处理了当其中一棵树的某个节点为空时的正确合并行为。

你的代码存在的问题与改进:

  1. 原地修改问题(常见误区):

    • 问题: 你的代码 t1->val += t2->val; 以及后续将合并结果直接赋给 t1->leftt1->right,这意味着你是在 原地修改t1 这棵树,并将其作为结果返回。

    • 误区: 题目通常会要求“合并成一棵 新的 二叉树”,这意味着你不能修改原有的输入树。如果你在面试中这样操作,面试官可能会追问你是否修改了原树。虽然某些题目允许原地修改以节省空间,但默认情况下,最好创建新节点。

    • 如何避免: 明确题目要求是原地修改还是生成新树。如果题目没有明确说明,通常应生成新树。

改进后的代码(生成新树):

为了避免原地修改原树,我们应该在合并时创建新的节点。

#include <stdlib.h> // For malloc

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

/**
 * @brief 合并两棵二叉树,生成一棵新的二叉树
 *
 * @param t1 TreeNode类,第一棵树的根节点
 * @param t2 TreeNode类,第二棵树的根节点
 * @return TreeNode类,返回合并后新树的根节点
 */
struct TreeNode *mergeTrees(struct TreeNode *t1, struct TreeNode *t2) {
    // 如果t1和t2都为空,则合并结果为空
    if (t1 == NULL && t2 == NULL) {
        return NULL;
    }

    // 如果t1为空,t2不为空,则新树的节点直接是t2的节点
    if (t1 == NULL) {
        return t2; // 这里可以理解为直接挂载t2的子树,因为t2的子树也会被递归处理
    }
    // 如果t2为空,t1不为空,则新树的节点直接是t1的节点
    if (t2 == NULL) {
        return t1; // 同理
    }

    // 如果t1和t2都不为空,则创建新节点,并将其值设为t1和t2节点值之和
    struct TreeNode *new_node = (struct TreeNode *)malloc(sizeof(struct TreeNode));
    if (new_node == NULL) {
        fprintf(stderr, "Memory allocation failed for new_node in mergeTrees.\n");
        exit(EXIT_FAILURE);
    }
    new_node->val = t1->val + t2->val;

    // 递归合并左右子树,并将结果赋给新节点的左右孩子
    new_node->left = mergeTrees(t1->left, t2->left);
    new_node->right = mergeTrees(t1->right, t2->right);

    return new_node;
}

注意: 如果题目明确允许原地修改,你的原始代码是完全正确的,且能节省内存。但在不确定时,生成新树是更安全的做法。

5. 二叉树的镜像

二叉树的镜像操作是指将二叉树的左右子树进行交换,从而得到一个关于根节点对称的镜像树。

5.1 BM33 二叉树的镜像

题目要求: 给定一个二叉树的根节点 pRoot,返回其镜像。

镜像操作定义: 递归地交换每个节点的左右子节点。

你的代码分析:

// BM33二叉树镜像.c
struct TreeNode *Mirror(struct TreeNode *pRoot)
{
    if(pRoot==NULL) return ; // 问题1:返回类型不匹配
    struct TreeNode* temp  = pRoot->right;
    pRoot->right  = pRoot->left;
    pRoot->left = temp;
    Mirror(pRoot->left);
    Mirror(pRoot->right);    
    return pRoot;
}

你的代码优点:

  • 核心逻辑正确: 交换左右子树的逻辑 struct TreeNode* temp = pRoot->right; pRoot->right = pRoot->left; pRoot->left = temp; 是完全正确的。

  • 递归调用正确: 对左右子树进行递归调用 Mirror(pRoot->left); Mirror(pRoot->right); 确保了所有子树都被镜像。

  • 原地修改: 该操作通常是原地修改,你的代码也符合这一点。

你的代码存在的问题与改进:

  1. void 返回类型误区:

    • 问题:if(pRoot==NULL) return ; 这一行,当 pRootNULL 时,你使用了 return;。然而,函数的返回类型是 struct TreeNode *,这意味着当 pRootNULL 时,函数应该返回 NULL。这在C语言中是一个编译警告(或者某些编译器会报错),并且逻辑上不严谨。

    • 误区: 习惯了 void 函数的递归终止条件,忘记了带返回值的递归函数在终止时也需要返回一个符合类型的值。

    • 如何避免: 仔细检查函数签名,确保所有返回路径都返回正确的类型。

改进后的代码:

#include <stdlib.h>

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

/**
 * @brief 对二叉树进行镜像操作
 *
 * @param pRoot TreeNode类,二叉树的根节点
 * @return TreeNode类,返回镜像后的二叉树的根节点
 */
struct TreeNode *Mirror(struct TreeNode *pRoot) {
    // 递归终止条件:如果节点为空,直接返回NULL
    if (pRoot == NULL) {
        return NULL; // 修正:当pRoot为NULL时,返回NULL
    }

    // 交换当前节点的左右子节点
    struct TreeNode *temp = pRoot->left;
    pRoot->left = pRoot->right;
    pRoot->right = temp;

    // 递归地对左右子树进行镜像操作
    Mirror(pRoot->left);
    Mirror(pRoot->right);

    // 返回当前节点(根节点),因为它现在是镜像树的根
    return pRoot;
}

总结: 二叉树镜像操作是二叉树递归应用的一个经典例子,关键在于理解每个节点的操作是独立的,且递归地作用于其子节点。

------------------------------------------------------------------------------------------------------------跟新于2025.4.1晚7.47
 

二叉树核心点精讲(2):高级特性与复杂问题

承接上文对二叉树基础遍历与操作的讲解,本部分我们将深入探讨二叉树的几种重要特性判断(二叉搜索树、完全二叉树、平衡二叉树),以及在面试中频繁出现的复杂问题,如查找最近公共祖先和二叉树的序列化与反序列化。我们将继续结合你提供的代码,剖析常见误区,提供优化建议,并给出详尽的C语言实现。

6. 二叉搜索树的判断

二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树,它具有以下性质:

  • 若左子树不为空,则左子树上所有节点的值均小于根节点的值。

  • 若右子树不为空,则右子树上所有节点的值均大于根节点的值。

  • 左右子树本身也分别是二叉搜索树。

关键性质: 中序遍历二叉搜索树得到的结果是一个递增序列。

6.1 BM34 判断是不是二叉搜索树

题目要求: 给定一个二叉树的根节点 root,判断它是否是一个有效的二叉搜索树。

你的代码分析:

// BM34是不是而搜索树.c
#include<limits.h> // For LONG_MIN, LONG_MAX

bool doFunc(struct TreeNode *root, long minV, long maxV)
{
    if(root==NULL) return true; // 递归终止条件
    if(root->val<minV ||root->val>maxV){ // 检查当前节点值是否在有效范围内
        return false;
    }
    // 递归检查左右子树,并更新子树的有效范围
    return doFunc(root->left,minV,root->val)&& doFunc(root->right ,root->val,maxV);
}

bool isValidBST(struct TreeNode *root)
{
    // write code here
    return doFunc(root,LONG_MIN,LONG_MAX); // 初始调用,根节点值的范围是所有可能的long型整数
}

你的代码优点:

  • 核心思想正确: 你的代码采用了自顶向下、递归检查节点值范围的思路,这是判断二叉搜索树有效性的标准且高效的方法。

  • 边界条件处理得当: if(root==NULL) return true; 处理了空节点作为有效BST的情况。

  • 初始范围正确: 使用 LONG_MINLONG_MAX 作为根节点的初始上下界,确保了对整个树的正确性检查。

你的代码存在的问题与改进:

几乎没有问题,这是一个非常标准的正确解法!

可以改进的地方(细节和严谨性):

  1. 等值问题(易错点):

    • 问题: 你的判断条件是 root->val < minV || root->val > maxV。这表明你的BST不允许节点值重复。然而,有些BST的定义允许重复值,有些则不允许。通常情况下,严格的BST定义是“左子树所有节点值小于根节点,右子树所有节点值大于根节点”,即不允许重复。如果题目明确允许重复值,那么 root->val <= maxVroot->val >= minV 可能会是适当的调整,但这需要根据具体的题目要求来定。

    • 误区: 对BST的“等于”情况考虑不足。在面试中,务必和面试官确认BST是否允许重复值。如果允许重复值,通常是左子树 val <= root->val,右子树 val >= root->val(或者反过来,但要保持一致)。如果不允许,则 val < root->valval > root->val 是正确的。你的代码符合不允许重复值的严格BST定义。

    • 你的代码在此题中是正确的,因为牛客的BM系列题目通常采用严格的BST定义。

  2. 函数命名: 再次提及,doFunc 仍不够语义化。可以考虑 isValidBSTRecursivecheckBSTRange

总结: 判断二叉搜索树的关键在于维护一个节点值的有效范围。在递归遍历时,左子树的有效范围是 (minV, root->val),右子树的有效范围是 (root->val, maxV)。这是一个非常重要的知识点,你的实现非常出色。

7. 完全二叉树的判断

完全二叉树(Complete Binary Tree)定义:若设二叉树的深度为h,除第h层外,其它各层(1到h−1层)的节点数都达到最大个数,第h层所有的节点都连续集中在最左边。

判断思路: 通常使用**层次遍历(BFS)**来判断。

  1. 从根节点开始层次遍历。

  2. 在遍历过程中,如果遇到一个空节点,那么此后的所有节点都必须是空节点,否则就不是完全二叉树。

  3. 如果遇到非空节点,但其左子节点为空而右子节点不为空,则不是完全二叉树。

7.1 BM35 判断是不是完全二叉树

题目要求: 给定一个二叉树的根节点 root,判断它是否是一个完全二叉树。

你的代码分析:

// BM35完全二叉树.c
/**
 * struct TreeNode {
 *	int val;
 *	struct TreeNode *left;
 *	struct TreeNode *right;
 * };
 */
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param root TreeNode类
 * @return bool布尔型
 */
bool isCompleteTree(struct TreeNode* root ) {
    // write code here


}

你的代码存在的问题与改进:

  1. 代码缺失: 你的 isCompleteTree 函数体为空。这表明你可能还没有来得及实现这部分。

实现思路(层次遍历/BFS):

  1. 使用一个队列(Queue)来进行层次遍历。

  2. 将根节点入队。

  3. 设置一个标志 has_null,初始为 false

  4. 当队列不为空时:

    • 出队一个节点 current

    • 如果 current 为空,将 has_null 设置为 true

    • 如果 current 不为空:

      • 如果 has_null 已经为 true,但 current 却不是空节点,说明在空节点之后又出现了非空节点,不符合完全二叉树的定义,返回 false

      • current 的左子节点和右子节点依次入队。

改进后的代码:

需要先定义一个队列结构。

#include <stdlib.h> // For malloc, free
#include <stdbool.h> // For bool

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

// --- 队列结构定义 ---
typedef struct {
    struct TreeNode **array;
    int front;
    int rear;
    int size;
    int capacity;
} Queue;

// 创建队列
Queue* createQueue(int capacity) {
    Queue *q = (Queue *)malloc(sizeof(Queue));
    if (q == NULL) return NULL;
    q->capacity = capacity;
    q->front = 0;
    q->size = 0;
    q->rear = capacity - 1; // rear 指向最后一个元素的前一个位置,方便循环队列的计算
    q->array = (struct TreeNode **)malloc(q->capacity * sizeof(struct TreeNode *));
    if (q->array == NULL) {
        free(q);
        return NULL;
    }
    return q;
}

// 队列是否为空
bool isQueueEmpty(Queue *q) {
    return (q->size == 0);
}

// 队列是否已满
bool isQueueFull(Queue *q) {
    return (q->size == q->capacity);
}

// 扩容队列
bool expandQueue(Queue *q) {
    int old_capacity = q->capacity;
    int new_capacity = old_capacity * 2;
    if (new_capacity == 0) new_capacity = 10; // 避免0容量扩容

    struct TreeNode **new_array = (struct TreeNode **)realloc(q->array, new_capacity * sizeof(struct TreeNode *));
    if (new_array == NULL) return false;

    // 如果数据不是连续的,需要重新排列
    if (q->front > q->rear) { // 队列数据循环到了数组末尾又从头开始
        // 将后半部分数据移动到新数组的末尾
        // 例如:[..., D, E, F, A, B, C, ...] (old_capacity)
        // 变成:[A, B, C, ..., D, E, F, ...] (new_capacity)
        for (int i = 0; i < q->front; ++i) {
            new_array[old_capacity + i] = new_array[i];
        }
        q->rear += old_capacity; // 更新rear
    }
    q->array = new_array;
    q->capacity = new_capacity;
    return true;
}


// 入队
void enqueue(Queue *q, struct TreeNode *item) {
    if (isQueueFull(q)) {
        if (!expandQueue(q)) {
            fprintf(stderr, "Queue expansion failed!\n");
            exit(EXIT_FAILURE);
        }
    }
    q->rear = (q->rear + 1) % q->capacity;
    q->array[q->rear] = item;
    q->size++;
}

// 出队
struct TreeNode* dequeue(Queue *q) {
    if (isQueueEmpty(q)) {
        return NULL; // 或者报错
    }
    struct TreeNode *item = q->array[q->front];
    q->front = (q->front + 1) % q->capacity;
    q->size--;
    return item;
}

// 销毁队列
void destroyQueue(Queue *q) {
    if (q == NULL) return;
    free(q->array);
    free(q);
}

// --- 完全二叉树判断函数 ---
/**
 * @brief 判断一棵二叉树是否是完全二叉树
 *
 * @param root TreeNode类,二叉树的根节点
 * @return bool布尔型,如果是完全二叉树返回true,否则返回false
 */
bool isCompleteTree(struct TreeNode* root) {
    if (root == NULL) {
        return true; // 空树是完全二叉树
    }

    Queue *q = createQueue(10); // 初始队列容量
    if (q == NULL) {
        fprintf(stderr, "Failed to create queue for isCompleteTree.\n");
        return false;
    }

    enqueue(q, root);
    bool has_null = false; // 标志位:是否遇到了第一个空节点

    while (!isQueueEmpty(q)) {
        struct TreeNode *current = dequeue(q);

        if (current == NULL) {
            // 如果当前节点是空,设置has_null为true,并继续处理队列中剩余的节点
            has_null = true;
        } else {
            // 如果has_null已经为true,但当前节点却不为空,则不是完全二叉树
            if (has_null) {
                destroyQueue(q);
                return false;
            }
            // 如果当前节点不为空,将其左右孩子入队(即使为空也要入队,用于后续判断)
            enqueue(q, current->left);
            enqueue(q, current->right);
        }
    }

    destroyQueue(q); // 释放队列内存
    return true; // 遍历结束,符合完全二叉树定义
}

总结: 判断完全二叉树的核心在于BFS遍历过程中,一旦遇到空节点,之后的所有节点都必须是空节点。同时,任何非空节点的右子节点不能在左子节点为空的情况下出现。

8. 平衡二叉树的判断

平衡二叉树(Balanced Binary Tree)又称AVL树,是一种特殊的二叉搜索树,它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

8.1 BM36 判断是不是平衡二叉树

题目要求: 给定一个二叉树的根节点 pRoot,判断它是否是一个平衡二叉树。

你的代码分析:

// BM36是不是平衡二叉树.c
/**
 * struct TreeNode {
 *	int val;
 *	struct TreeNode *left;
 *	struct TreeNode *right;
 * };
 */
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param pRoot TreeNode类
 * @return bool布尔型
 */
int getD(struct TreeNode* root ){ // 计算深度函数

    if(root==NULL) return 0 ;
    int lLen = getD(root->left);
    int rLen = getD(root->right);
    return lLen >rLen?lLen+1:rLen+1;
}

bool IsBalanced_Solution(struct TreeNode* pRoot ) {
    // write code here

    int lLen =  getD(pRoot->left); // 问题1:对空树根节点直接调用getD会导致错误
    int rLen  = getD(pRoot->right);
    // 问题2:递归判断条件缺失对pRoot本身的空判断,且abs函数需要引入
    return IsBalanced_Solution(pRoot->left ) && IsBalanced_Solution(pRoot->right) && (abs(lLen-rLen)<=1);
}

你的代码优点:

  • getD 函数正确: getD 函数与之前判断最大深度的函数一致,正确地计算了子树的高度。

  • 分治思想: 你尝试用递归方式 IsBalanced_Solution(pRoot->left ) && IsBalanced_Solution(pRoot->right) 来检查子树的平衡性,这是正确的方向。

  • 高度差判断: (abs(lLen-rLen)<=1) 抓住了平衡二叉树的核心条件。

你的代码存在的问题与改进:

  1. 根节点为空的边界条件处理:

    • 问题: IsBalanced_Solution 函数缺少对 pRootNULL 的初始判断。如果传入的 pRootNULL,直接调用 getD(pRoot->left) 会导致空指针解引用错误。

    • 误区: 认为函数的参数一定是非空的。在处理树的问题时,一定要最优先考虑空节点的情况。

    • 如何避免: 在函数入口处添加 if (pRoot == NULL) return true;。空树是平衡的。

  2. 重复计算高度:

    • 问题: 你的 IsBalanced_Solution 在每次递归调用时,都会通过 getD 重新计算左右子树的高度。这意味着对于一个节点,它的高度可能被重复计算多次(在其父节点和祖父节点的 getD 调用中)。这导致了大量重复工作,时间复杂度较高。

    • 误区: 将高度计算和平衡性判断分开进行,导致了效率低下。

    • 如何避免: 将高度计算和平衡性判断结合在一个递归函数中,从底向上进行。在计算子树高度的同时,判断子树是否平衡。如果子树不平衡,可以直接返回一个特殊值(例如 -1)表示不平衡,从而剪枝。

  3. 缺少 abs 函数的头文件:

    • 问题: abs 函数通常在 <stdlib.h><math.h> 中定义。你的代码没有包含这些头文件。

    • 如何避免: 引入必要的头文件。

改进后的代码(优化:一次遍历判断高度和平衡性):

这种方法通过一个递归函数,同时返回子树的高度和子树是否平衡的信息。如果子树不平衡,返回一个负值(例如-1),否则返回其高度。

#include <stdlib.h> // For abs, exit, fprintf
#include <stdbool.h> // For bool
#include <math.h> // For fmax (可选,也可以用三元运算符)

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

/**
 * @brief 辅助函数:递归计算二叉树的高度,并在过程中判断是否平衡
 * 如果子树平衡,返回其高度;如果不平衡,返回-1。
 *
 * @param root 当前递归到的节点
 * @return int 如果子树平衡,返回其高度;否则返回-1
 */
int getHeightAndCheckBalance(struct TreeNode* root) {
    if (root == NULL) {
        return 0; // 空节点高度为0,且是平衡的
    }

    // 递归获取左子树的高度和平衡性
    int leftHeight = getHeightAndCheckBalance(root->left);
    if (leftHeight == -1) {
        return -1; // 左子树不平衡,则整个树也不平衡
    }

    // 递归获取右子树的高度和平衡性
    int rightHeight = getHeightAndCheckBalance(root->right);
    if (rightHeight == -1) {
        return -1; // 右子树不平衡,则整个树也不平衡
    }

    // 检查当前节点的左右子树的高度差是否超过1
    if (abs(leftHeight - rightHeight) > 1) {
        return -1; // 当前子树不平衡
    }

    // 如果当前子树平衡,返回其高度(左右子树高度的较大值加1)
    return (int)fmax(leftHeight, rightHeight) + 1;
}

/**
 * @brief 判断一棵二叉树是否是平衡二叉树
 *
 * @param pRoot TreeNode类,二叉树的根节点
 * @return bool布尔型,如果是平衡二叉树返回true,否则返回false
 */
bool IsBalanced_Solution(struct TreeNode* pRoot) {
    // 调用辅助函数,如果返回-1则表示不平衡
    return getHeightAndCheckBalance(pRoot) != -1;
}

总结: 平衡二叉树的判断是一个典型的需要“自底向上”进行递归的题目。将高度计算和平衡性判断结合在一次递归中,可以避免重复计算,大大提高效率。

9. 最近公共祖先

最近公共祖先(Lowest Common Ancestor, LCA)是指在二叉树中,对于两个给定的节点 p 和 q,它们的最近公共祖先是离这两个节点最近的,同时是它们共同祖先的节点。

9.1 BM37 最近公共祖先

题目要求: 给定一个二叉树的根节点 root,以及树中的两个节点的值 pq,找到这两个节点的最近公共祖先。

你的代码分析:

// BM37最近公共祖先.c
/**
 * struct TreeNode {
 *	int val;
 *	struct TreeNode *left;
 *	struct TreeNode *right;
 * };
 */
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param root TreeNode类
 * @param p int整型
 * @param q int整型
 * @return int整型
 */
int lowestCommonAncestor(struct TreeNode *root, int p, int q)
{
    // write code here


}

你的代码存在的问题与改进:

  1. 代码缺失: 你的 lowestCommonAncestor 函数体为空,需要实现。

实现思路(递归):

这道题有两种主要情况:

  1. 普通二叉树的LCA: 节点 pq 可能在树中的任何位置,没有大小顺序关系。

  2. 二叉搜索树(BST)的LCA: 节点 pq 具有大小顺序关系,可以利用BST的性质进行优化。

牛客的BM系列题目通常会区分普通二叉树和二叉搜索树的LCA。根据题目列表,你提到的是 BM15二叉搜索树的最近公共祖先BM16在二叉树中找到两个节点的最近公共祖先。由于你提供的是 BM37最近公共祖先.c 且其文件名未明确说明是BST,这里我们将假设是普通二叉树的LCA,因为它更具普遍性。如果是BST的LCA,解法会更简洁。

普通二叉树LCA的递归思路:

一个经典的后序遍历(或自底向上)的思路:

  • 基本情况:

    • 如果 rootNULL,或者 root 的值等于 pq,则返回 root

  • 递归情况:

    • 递归地在左子树中查找 pq (left_lca)。

    • 递归地在右子树中查找 pq (right_lca)。

  • 处理结果:

    • 如果 left_lcaright_lca 都不为空,说明 pq 分别位于 root 的左右子树中,那么 root 就是它们的最近公共祖先,返回 root

    • 如果 left_lca 不为空而 right_lca 为空,说明 pq 都位于 root 的左子树中(或者 pq 本身就是 left_lca),那么 left_lca 就是它们的最近公共祖先,返回 left_lca

    • 如果 right_lca 不为空而 left_lca 为空,同理,返回 right_lca

    • 如果 left_lcaright_lca 都为空,说明 root 的子树中没有找到 pq,返回 NULL

改进后的代码(普通二叉树LCA):

#include <stdlib.h> // For exit, fprintf

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

/**
 * @brief 在普通二叉树中找到两个节点的最近公共祖先
 *
 * @param root TreeNode类,二叉树的根节点
 * @param p int整型,第一个目标节点的值
 * @param q int整型,第二个目标节点的值
 * @return int整型,返回最近公共祖先的节点值 (注意:题目要求返回int型,而不是TreeNode*)
 */
int lowestCommonAncestor(struct TreeNode *root, int p, int q) {
    // 基本情况:
    // 1. 如果当前节点为空,返回NULL
    // 2. 如果当前节点的值等于p或q,那么当前节点就是p或q本身,也可能是LCA
    if (root == NULL || root->val == p || root->val == q) {
        // 返回当前节点指针,后续需要取val
        // 由于题目要求返回int整型,这里直接返回节点值
        // 但为了递归逻辑的完整性,通常函数会返回TreeNode*,然后再在顶层取val
        // 这里为了遵循原题接口,我们先返回一个特殊值,或者需要改变接口设计
        // 鉴于C语言接口的限制,我们让递归函数返回TreeNode*,外部函数再取val
        // 所以我们需要一个新的辅助函数。
        // 或者,如果题目的意思是返回找到的LCA的val,且保证p和q一定在树中,
        // 那么可以在找到LCA的TreeNode*后,直接返回其val。
        return (root != NULL) ? root->val : -1; // 假设-1表示未找到或错误
    }

    // 递归在左子树中查找p和q的LCA
    int left_lca_val = lowestCommonAncestor(root->left, p, q);
    // 递归在右子树中查找p和q的LCA
    int right_lca_val = lowestCommonAncestor(root->right, p, q);

    // 情况1:p和q分别在左右子树中,那么当前root就是LCA
    if (left_lca_val != -1 && right_lca_val != -1) {
        return root->val;
    }
    // 情况2:p和q都在左子树中(或者left_lca_val就是p或q),LCA在左子树中
    else if (left_lca_val != -1) {
        return left_lca_val;
    }
    // 情况3:p和q都在右子树中(或者right_lca_val就是p或q),LCA在右子树中
    else if (right_lca_val != -1) {
        return right_lca_val;
    }
    // 情况4:左右子树都没有找到p或q
    else {
        return -1; // 表示未找到
    }
}


// **更标准的LCA递归函数,返回TreeNode*,外部再取val**
// 这是面试中更常见的LCA函数设计,因为它返回了节点本身,更灵活
struct TreeNode* findLCA(struct TreeNode* root, int p_val, int q_val) {
    // 递归终止条件:
    // 1. 如果当前节点为空,则返回NULL
    // 2. 如果当前节点的值等于p_val或q_val,说明找到了其中一个目标节点,
    //    或者当前节点就是LCA(如果另一个节点在其子树中)
    if (root == NULL || root->val == p_val || root->val == q_val) {
        return root;
    }

    // 递归地在左子树中查找p和q的LCA
    struct TreeNode* left_lca = findLCA(root->left, p_val, q_val);
    // 递归地在右子树中查找p和q的LCA
    struct TreeNode* right_lca = findLCA(root->right, p_val, q_val);

    // 根据左右子树的查找结果进行判断
    if (left_lca != NULL && right_lca != NULL) {
        // 如果左右子树都找到了目标节点(或其LCA),说明p和q分别在root的左右子树中,
        // 那么root就是它们的最近公共祖先。
        return root;
    } else if (left_lca != NULL) {
        // 如果只有左子树找到了结果,说明p和q都在左子树中,或者left_lca就是p或q本身。
        // 那么left_lca就是它们的最近公共祖先。
        return left_lca;
    } else {
        // 如果只有右子树找到了结果(或right_lca就是p或q本身),
        // 或者左右子树都没有找到(此时left_lca和right_lca都为NULL),
        // 统一返回right_lca (无论是找到的LCA还是NULL)。
        return right_lca;
    }
}

// 针对牛客题目要求返回int整型的情况,可以这样封装:
int lowestCommonAncestor_Wrapper(struct TreeNode *root, int p, int q) {
    struct TreeNode* lca_node = findLCA(root, p, q);
    if (lca_node != NULL) {
        return lca_node->val;
    }
    // 如果题目保证p和q一定存在于树中,这个分支通常不会执行到。
    // 如果不保证,则需要根据题目定义返回一个“未找到”的特殊值,比如INT_MIN或抛出错误。
    // 假设题目保证存在,这里为了避免返回不确定的值,可以添加错误处理
    fprintf(stderr, "Error: p or q not found in the tree, or unexpected NULL LCA.\n");
    exit(EXIT_FAILURE); // 或者返回一个表示错误的值
}

如果是二叉搜索树(BST)的LCA:

如果题目明确是二叉搜索树,那么利用BST的性质,LCA的查找会更简单:

// BST版 LCA
struct TreeNode* lowestCommonAncestor_BST(struct TreeNode* root, int p, int q) {
    if (root == NULL) {
        return NULL;
    }

    // 如果p和q都比root的值小,说明LCA在左子树
    if (root->val > p && root->val > q) {
        return lowestCommonAncestor_BST(root->left, p, q);
    }
    // 如果p和q都比root的值大,说明LCA在右子树
    else if (root->val < p && root->val < q) {
        return lowestCommonAncestor_BST(root->right, p, q);
    }
    // 否则,说明p和q一个在左子树、一个在右子树,或者root就是p或q之一,
    // 那么当前root就是LCA
    else {
        return root;
    }
}

// 针对牛客题目要求返回int整型的情况
int lowestCommonAncestor_BST_Wrapper(struct TreeNode *root, int p, int q) {
    struct TreeNode* lca_node = lowestCommonAncestor_BST(root, p, q);
    if (lca_node != NULL) {
        return lca_node->val;
    }
    fprintf(stderr, "Error: p or q not found in BST, or unexpected NULL LCA.\n");
    exit(EXIT_FAILURE);
}

总结: LCA问题是二叉树中一个重要的应用。普通二叉树的LCA需要考虑节点在左右子树的分布情况,通常采用“自底向上”的递归策略。二叉搜索树的LCA则可以利用其有序性,通过比较节点值进行“自顶向下”的判断,效率更高。

10. 二叉树的序列化与反序列化

二叉树的序列化(Serialization)是指将二叉树的结构和节点值转换为一个线性的字符串或数组,以便于存储或传输。反序列化(Deserialization)则是指将这个线性表示恢复为原来的二叉树结构。

10.1 BM17 序列化二叉树

题目要求: 请实现两个函数,分别用来序列化和反序列化二叉树。

你的代码分析:

你没有提供此题的代码,因此这里直接给出通用的实现。

实现思路:

序列化和反序列化通常采用以下两种策略:

  1. 前序遍历序列化(常见):

    • 在遍历时,如果遇到空节点,用一个特殊标记(例如#null)表示。

    • 节点值之间用分隔符(例如逗号,)隔开。

    • 示例:1,2,#,#,3,4,#,#,5,#,# 表示根节点为1,左孩子为2,右孩子为3。2是叶子节点,3的左孩子是4,右孩子是5。

  2. 层次遍历序列化:

    • 使用队列进行层次遍历。

    • 空节点也入队,并用特殊标记表示。

这里我们以前序遍历为例进行实现,因为它在反序列化时通常比较直接。

序列化函数 serialize

#include <stdio.h> // For sprintf, sscanf
#include <stdlib.h> // For malloc, realloc, free
#include <string.h> // For strcat, strcpy

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

// 辅助函数:将整数转换为字符串
char* intToString(int val) {
    char* str = (char*)malloc(12 * sizeof(char)); // 足够存储INT_MIN/MAX和负号
    if (str == NULL) {
        fprintf(stderr, "Memory allocation failed for intToString.\n");
        exit(EXIT_FAILURE);
    }
    sprintf(str, "%d", val);
    return str;
}

/**
 * @brief 递归辅助函数:将二叉树前序遍历并转换为字符串
 *
 * @param root 当前节点
 * @param str_buffer 指向动态字符串缓冲区的指针的指针
 * @param current_len 指向当前字符串长度的指针
 * @param buffer_capacity 指向缓冲区容量的指针
 */
void serialize_recursive(struct TreeNode* root, char** str_buffer, int* current_len, int* buffer_capacity) {
    char temp_str[20]; // 临时字符串,用于存放节点值或空标记

    if (root == NULL) {
        strcpy(temp_str, "#"); // 空节点用 '#' 表示
    } else {
        sprintf(temp_str, "%d", root->val); // 非空节点转换为字符串
    }

    // 检查缓冲区是否足够存储当前节点字符串和分隔符
    int needed_len = strlen(temp_str);
    if (*current_len + needed_len + 1 >= *buffer_capacity) { // +1 for ',' or null terminator
        *buffer_capacity = (*buffer_capacity == 0) ? 100 : (*buffer_capacity * 2); // 扩容
        char* new_buffer = (char*)realloc(*str_buffer, *buffer_capacity * sizeof(char));
        if (new_buffer == NULL) {
            fprintf(stderr, "Memory reallocation failed in serialize_recursive.\n");
            exit(EXIT_FAILURE);
        }
        *str_buffer = new_buffer;
    }

    // 将当前节点字符串添加到缓冲区
    if (*current_len > 0) { // 如果不是第一个节点,先添加分隔符
        strcat(*str_buffer, ",");
        (*current_len)++;
    }
    strcat(*str_buffer, temp_str);
    *current_len += needed_len;

    if (root != NULL) {
        // 递归序列化左子树和右子树
        serialize_recursive(root->left, str_buffer, current_len, buffer_capacity);
        serialize_recursive(root->right, str_buffer, current_len, buffer_capacity);
    }
}


/**
 * @brief 序列化二叉树
 * @param root TreeNode类 树的根节点
 * @return char* 序列化后的字符串
 */
char* serialize(struct TreeNode* root) {
    char* str_buffer = NULL;
    int current_len = 0;
    int buffer_capacity = 0; // 初始容量为0,会在第一次调用时扩容

    // 调用递归函数进行序列化
    serialize_recursive(root, &str_buffer, &current_len, &buffer_capacity);

    // 返回最终的字符串
    return str_buffer;
}

反序列化函数 deserialize

反序列化通常需要一个指向当前解析位置的指针或索引,因为递归解析会前进这个位置。

// 辅助函数:将字符串分割成数组,方便解析
// 这是一个简化的分割函数,实际可能需要更健壮的实现
char** splitString(char* str, const char* delimiter, int* count) {
    char** result = NULL;
    int idx = 0;
    char* temp_str = strdup(str); // 复制一份字符串,因为strtok会修改原字符串
    if (temp_str == NULL) {
        fprintf(stderr, "Memory allocation failed for splitString temp_str.\n");
        exit(EXIT_FAILURE);
    }

    char* token = strtok(temp_str, delimiter);
    while (token != NULL) {
        result = (char**)realloc(result, (idx + 1) * sizeof(char*));
        if (result == NULL) {
            fprintf(stderr, "Memory reallocation failed for splitString result.\n");
            free(temp_str);
            exit(EXIT_FAILURE);
        }
        result[idx] = strdup(token); // 复制token
        if (result[idx] == NULL) {
            fprintf(stderr, "Memory allocation failed for splitString result[idx].\n");
            // 释放之前分配的内存
            for(int i = 0; i < idx; ++i) free(result[i]);
            free(result);
            free(temp_str);
            exit(EXIT_FAILURE);
        }
        idx++;
        token = strtok(NULL, delimiter);
    }
    *count = idx;
    free(temp_str);
    return result;
}

/**
 * @brief 递归辅助函数:从字符串数组中反序列化二叉树
 *
 * @param nodes 字符串数组,包含序列化后的节点信息
 * @param index 指向当前解析位置的指针
 * @param total_nodes 字符串数组的总节点数
 * @return struct TreeNode* 反序列化出的当前子树的根节点
 */
struct TreeNode* deserialize_recursive(char** nodes, int* index, int total_nodes) {
    // 如果索引越界,或者当前节点是空标记,则返回NULL
    if (*index >= total_nodes || strcmp(nodes[*index], "#") == 0) {
        (*index)++; // 移动到下一个节点
        return NULL;
    }

    // 创建新节点
    struct TreeNode* new_node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    if (new_node == NULL) {
        fprintf(stderr, "Memory allocation failed for new_node in deserialize_recursive.\n");
        exit(EXIT_FAILURE);
    }
    new_node->val = atoi(nodes[*index]); // 将字符串转换为整数值
    (*index)++; // 移动到下一个节点

    // 递归反序列化左子树和右子树
    new_node->left = deserialize_recursive(nodes, index, total_nodes);
    new_node->right = deserialize_recursive(nodes, index, total_nodes);

    return new_node;
}


/**
 * @brief 反序列化二叉树
 * @param str char* 序列化后的字符串
 * @return TreeNode类 恢复后的二叉树根节点
 */
struct TreeNode* deserialize(char* str) {
    if (str == NULL || strlen(str) == 0) {
        return NULL;
    }

    int total_nodes = 0;
    char** nodes_str = splitString(str, ",", &total_nodes); // 将字符串分割成数组

    if (nodes_str == NULL || total_nodes == 0) {
        return NULL;
    }

    int index = 0; // 用于跟踪当前解析到的位置
    struct TreeNode* root = deserialize_recursive(nodes_str, &index, total_nodes);

    // 释放splitString分配的内存
    for (int i = 0; i < total_nodes; i++) {
        free(nodes_str[i]);
    }
    free(nodes_str);

    return root;
}

总结: 序列化与反序列化是考察对二叉树遍历和内存管理能力的重要题目。前序遍历配合特殊标记 # 是一种常见的序列化方式,反序列化时通过一个全局或传递的索引来跟踪解析进度是关键。需要特别注意内存的动态分配和释放。

11. 重建二叉树

重建二叉树是指根据二叉树的两种遍历序列(通常是前序和中序,或中序和后序)来唯一确定并重建出原二叉树。

11.1 BM18 重建二叉树

题目要求: 给定二叉树的前序遍历和中序遍历结果,重建二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

你的代码分析:

你没有提供此题的代码,因此这里直接给出通用的实现。

实现思路:

核心思想是分治

  • 前序遍历的特点: 第一个元素永远是当前子树的根节点。

  • 中序遍历的特点: 根节点将中序遍历序列分割成两部分:左边的部分是左子树的中序遍历,右边的部分是右子树的中序遍历。

算法步骤:

  1. 确定根节点: 前序遍历的第一个元素就是根节点的值。

  2. 在中序遍历中找到根节点: 在中序遍历序列中找到这个根节点的值。

  3. 划分左右子树:

    • 根节点在中序遍历中的位置,其左边的所有元素构成左子树的中序遍历序列。

    • 根节点在中序遍历中的位置,其右边的所有元素构成右子树的中序遍历序列。

    • 根据左子树中序遍历的长度,从前序遍历中截取相应的长度作为左子树的前序遍历序列。

    • 剩余的前序遍历序列则作为右子树的前序遍历序列。

  4. 递归重建: 递归地对左子树和右子树进行相同的重建过程。

改进后的代码:

需要一个辅助函数来根据数组范围进行递归。

#include <stdlib.h> // For malloc, exit, fprintf

// 假设TreeNode结构体已在其他地方定义
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

/**
 * @brief 辅助函数:在中序遍历数组中查找目标值的索引
 *
 * @param inorder 中序遍历数组
 * @param in_start 中序遍历的起始索引
 * @param in_end 中序遍历的结束索引
 * @param target 目标值
 * @return int 目标值在数组中的索引,如果未找到返回-1
 */
int find_index_in_inorder(int* inorder, int in_start, int in_end, int target) {
    for (int i = in_start; i <= in_end; i++) {
        if (inorder[i] == target) {
            return i;
        }
    }
    return -1; // 应该总是能找到,因为题目保证不含重复数字
}

/**
 * @brief 递归辅助函数:根据前序遍历和中序遍历结果重建二叉树
 *
 * @param preorder 前序遍历数组
 * @param pre_start 前序遍历的起始索引
 * @param pre_end 前序遍历的结束索引
 * @param inorder 中序遍历数组
 * @param in_start 中序遍历的起始索引
 * @param in_end 中序遍历的结束索引
 * @return struct TreeNode* 重建出的当前子树的根节点
 */
struct TreeNode* reConstructBinaryTree_recursive(int* preorder, int pre_start, int pre_end,
                                                int* inorder, int in_start, int in_end) {
    // 递归终止条件:如果前序或中序遍历的范围无效,或者为空
    if (pre_start > pre_end || in_start > in_end) {
        return NULL;
    }

    // 前序遍历的第一个元素是当前子树的根节点
    struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    if (root == NULL) {
        fprintf(stderr, "Memory allocation failed for new_node in reConstructBinaryTree_recursive.\n");
        exit(EXIT_FAILURE);
    }
    root->val = preorder[pre_start];

    // 在中序遍历中找到根节点的位置
    int root_in_inorder_index = find_index_in_inorder(inorder, in_start, in_end, root->val);
    if (root_in_inorder_index == -1) {
        // 这通常不应该发生,因为题目保证有效输入
        fprintf(stderr, "Error: Root value not found in inorder traversal.\n");
        free(root);
        return NULL;
    }

    // 计算左子树的节点数量
    int left_subtree_size = root_in_inorder_index - in_start;

    // 递归重建左子树
    root->left = reConstructBinaryTree_recursive(
        preorder, pre_start + 1, pre_start + left_subtree_size,
        inorder, in_start, root_in_inorder_index - 1
    );

    // 递归重建右子树
    root->right = reConstructBinaryTree_recursive(
        preorder, pre_start + left_subtree_size + 1, pre_end,
        inorder, root_in_inorder_index + 1, in_end
    );

    return root;
}

/**
 * @brief 根据前序遍历和中序遍历重建二叉树
 *
 * @param pre int* 前序遍历数组
 * @param pre_len int 前序遍历数组的长度
 * @param vin int* 中序遍历数组
 * @param vin_len int 中序遍历数组的长度
 * @return TreeNode类* 重建后的二叉树根节点
 */
struct TreeNode* reConstructBinaryTree(int* pre, int pre_len, int* vin, int vin_len) {
    if (pre == NULL || pre_len == 0 || vin == NULL || vin_len == 0 || pre_len != vin_len) {
        return NULL;
    }

    return reConstructBinaryTree_recursive(pre, 0, pre_len - 1, vin, 0, vin_len - 1);
}

总结: 重建二叉树是分治思想的典型应用。理解前序遍历和中序遍历的特点,并通过根节点在中序遍历中的位置来划分左右子树,是解决这类问题的关键。

12. 输出二叉树的右视图

二叉树的右视图是指从二叉树的右侧观察时,可以看到的节点集合。

12.1 BM19 输出二叉树的右视图

题目要求: 给定一个二叉树的根节点 root,返回其右视图的节点值。

你的代码分析:

你没有提供此题的代码,因此这里直接给出通用的实现。

实现思路:

右视图的本质是每层最右边的节点。因此,可以使用**层次遍历(BFS)深度优先遍历(DFS)**来解决。

层次遍历(BFS)思路:

  1. 使用队列进行层次遍历。

  2. 在每层遍历开始时,记录当前层有多少个节点。

  3. 遍历当前层的节点,每次出队一个节点。当遍历到当前层的最后一个节点时,它就是该层最右边的节点,将其值加入结果集。

  4. 将当前节点的左右子节点(如果非空)入队,为下一层做准备。

改进后的代码(BFS):

同样需要队列结构。

#include <stdlib.h> // For malloc, realloc, free
#include <stdbool.h> // For bool

// 假设TreeNode结构体和Queue结构体及相关函数已在其他地方定义

/**
 * @brief 输出二叉树的右视图(层次遍历/BFS)
 *
 * @param root TreeNode类,二叉树的根节点
 * @param returnSize 返回数组的实际元素个数
 * @return int* 返回一个包含右视图结果的整型一维数组
 */
int* rightSideView(struct TreeNode* root, int* returnSize) {
    int* res = NULL;
    int capacity = 0;
    *returnSize = 0;

    if (root == NULL) {
        return NULL;
    }

    Queue* q = createQueue(10); // 初始队列容量
    if (q == NULL) {
        fprintf(stderr, "Failed to create queue for rightSideView.\n");
        return NULL;
    }

    enqueue(q, root);

    while (!isQueueEmpty(q)) {
        int level_size = q->size; // 当前层节点数量

        for (int i = 0; i < level_size; i++) {
            struct TreeNode* current = dequeue(q);

            // 如果是当前层的最后一个节点,它就是右视图的一部分
            if (i == level_size - 1) {
                if (*returnSize >= capacity) {
                    capacity = (capacity == 0) ? 10 : (capacity * 2);
                    int *new_res = (int *)realloc(res, capacity * sizeof(int));
                    if (new_res == NULL) {
                        fprintf(stderr, "Memory reallocation failed in rightSideView.\n");
                        destroyQueue(q);
                        free(res);
                        exit(EXIT_FAILURE);
                    }
                    res = new_res;
                }
                res[(*returnSize)++] = current->val;
            }

            // 将左右子节点入队
            if (current->left != NULL) {
                enqueue(q, current->left);
            }
            if (current->right != NULL) {
                enqueue(q, current->right);
            }
        }
    }

    destroyQueue(q);

    // 最终调整结果数组大小
    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) {
             fprintf(stderr, "Final memory reallocation failed in rightSideView.\n");
        } else if (*returnSize == 0) {
            free(res);
            res = NULL;
        } else {
            res = final_res;
        }
    }
    return res;
}

深度优先遍历(DFS)思路:

右视图也可以通过深度优先遍历来实现,通常是优先遍历右子树

  1. 使用一个变量 max_depth 记录已经访问到的最大深度。

  2. 在递归遍历时,传入当前节点的深度 depth

  3. 如果当前深度 depth 大于 max_depth,说明这是当前深度第一次访问到的节点(因为优先遍历右子树,所以一定是该层最右边的节点),将其值加入结果集,并更新 max_depth

  4. 先递归遍历右子树,再递归遍历左子树。

改进后的代码(DFS):

#include <stdlib.h> // For malloc, realloc, free
#include <stdbool.h> // For bool

// 假设TreeNode结构体已在其他地方定义

/**
 * @brief 辅助函数:深度优先遍历获取二叉树的右视图
 *
 * @param root 当前节点
 * @param depth 当前节点的深度(根节点深度为0或1,取决于定义)
 * @param max_depth 指向当前已访问到的最大深度的指针
 * @param res 指向结果数组指针的指针
 * @param capacity 指向结果数组容量的指针
 * @param returnSize 指向已收集节点数量的指针
 */
void rightSideView_dfs_recursive(struct TreeNode* root, int depth, int* max_depth,
                                 int** res, int* capacity, int* returnSize) {
    if (root == NULL) {
        return;
    }

    // 如果当前深度大于已访问到的最大深度,说明这是该层最右边的节点
    if (depth > *max_depth) {
        if (*returnSize >= *capacity) {
            *capacity = (*capacity == 0) ? 10 : (*capacity * 2);
            int *new_res = (int *)realloc(*res, *capacity * sizeof(int));
            if (new_res == NULL) {
                fprintf(stderr, "Memory reallocation failed in rightSideView_dfs_recursive.\n");
                exit(EXIT_FAILURE);
            }
            *res = new_res;
        }
        (*res)[(*returnSize)++] = root->val;
        *max_depth = depth; // 更新最大深度
    }

    // 优先遍历右子树
    rightSideView_dfs_recursive(root->right, depth + 1, max_depth, res, capacity, returnSize);
    // 再遍历左子树
    rightSideView_dfs_recursive(root->left, depth + 1, max_depth, res, capacity, returnSize);
}

/**
 * @brief 输出二叉树的右视图(深度优先遍历/DFS)
 *
 * @param root TreeNode类,二叉树的根节点
 * @param returnSize 返回数组的实际元素个数
 * @return int* 返回一个包含右视图结果的整型一维数组
 */
int* rightSideView_dfs(struct TreeNode* root, int* returnSize) {
    int* res = NULL;
    int capacity = 0;
    *returnSize = 0;
    int max_depth = -1; // 记录已访问到的最大深度,初始化为-1或0

    rightSideView_dfs_recursive(root, 0, &max_depth, &res, &capacity, returnSize); // 根节点深度为0

    // 最终调整结果数组大小
    if (*returnSize < capacity) {
        int *final_res = (int *)realloc(res, *returnSize * sizeof(int));
        if (final_res == NULL && *returnSize > 0) {
             fprintf(stderr, "Final memory reallocation failed in rightSideView_dfs.\n");
        } else if (*returnSize == 0) {
            free(res);
            res = NULL;
        } else {
            res = final_res;
        }
    }
    return res;
}

总结: 右视图问题既可以用BFS也可以用DFS解决。BFS通过记录每层的最后一个节点来获得,DFS则通过优先遍历右子树并记录首次访问的深度来获得。两种方法各有优劣,掌握它们有助于灵活应对类似问题。

结语

至此,我们已经系统地探讨了牛客面试中常见的二叉树题目,包括基础遍历、深度计算、合并、镜像、判断BST/完全树/平衡树,以及最近公共祖先和序列化/反序列化、重建二叉树、右视图。

通过对你提交的代码进行分析,我们发现了一些程序员在练习中常见的误区:

  1. 硬编码数组大小: 动态数组应使用 malloc/realloc 动态管理内存,避免固定大小的限制。

  2. 函数命名不规范: 良好的函数命名能够提高代码的可读性和可维护性。

  3. 原地修改与创建新对象: 明确题目要求是原地操作还是生成新对象,不确定时通常选择生成新对象。

  4. 边界条件处理不严谨: 尤其是空指针的检查,是避免运行时错误的关键。

  5. 重复计算与优化: 在递归问题中,考虑如何通过一次递归(如高度和平衡性判断结合)避免重复计算,提高效率。

  6. 辅助数据结构的使用: 灵活运用栈和队列来解决迭代遍历、层次遍历等问题。

二叉树作为面试必考点,笔者在二刷这个知识点所有题型的同时,想着都刷了第二遍了,还不如写个技术贴分享出来自己的这么多手写代码,于是有了这篇贴文,觉得不错的还请三联支持

--------------------------------------------------------------------------------------------------------更新于2025.6./13上午10:21分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值