二叉树探秘:首个共同先祖与中序后继者的算法博弈

#王者杯·14天创作挑战营·第4期#
在算法的星河中,二叉树如同枝繁叶茂的智慧之树,每一道题目都是其上的璀璨果实。今日,我们将剖开两颗尤为耀眼的果实——首个共同先祖(LCA) 与 后继者(Inorder Successor)。前者是血脉相连的寻根之旅,后者是孤舟独行的秩序之问。无需代码,仅凭逻辑与图示,随我穿越递归的迷雾,解构分治的兵法,揭开二叉树最精妙的拓扑博弈!
问题一:首个共同先祖(LCA)—— 二叉树的血脉寻踪

设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索

🌳 问题核心
给定任意二叉树(非二叉搜索树)和两个节点 p 与 q,求其最近公共祖先(LCA)。关键约束:禁止额外存储节点,仅靠递归栈空间破局。

📜 示例具象化

    3
   / \
  5   1
 / \ / \
6  2 0  8
  / \
 7   4
  • 示例1p=5, q=1 → LCA=3(分属左右子树)

  • 示例2p=5, q=4 → LCA=5(节点自身是祖先)

⚔️ 算法设计:递归分治的拓扑兵法

  1. 递归基:若当前节点为 nullp 或 q,直接返回该节点。

  2. 左右分治

    • 左子树递归搜索 p 和 q,结果记为 left

    • 右子树递归搜索 p 和 q,结果记为 right

  3. 状态合并(后序遍历精髓)

    • left 与 right 均非空 → 当前节点为 LCA(p 和 q 分居左右)。

    • left 非空,right 为空 → 返回 left(LCA 在左子树)。

    • right 非空,left 为空 → 返回 right(LCA 在右子树)。

🔍 复杂度剖析

  • 时间复杂度O(n)(最坏遍历所有节点)。

  • 空间复杂度O(h)(递归栈深度,h 为树高)。

🎯 示例推演(p=5, q=4

  1. 从根节点 3 出发,递归左子树(5)和右子树(1)。

  2. 左子树 5

    • 左子树 6 返回 null(无 p 或 q)。

    • 右子树 2:递归其左子树 7null)、右子树 4(命中 q),返回 4

    • 节点 2 合并结果:left=nullright=4 → 返回 4

  3. 节点 5 合并结果:left=null6 的返回), right=42 的返回)→ 5 自身是 p,返回 5

  4. 根节点 3left=5(左子树结果), right=null(右子树无 p 或 q)→ 返回 5

关键洞见:递归的本质是 “自底向上回溯”,LCA 的判定发生于后序遍历的合并阶段。

题目程序:

#include <stdio.h>
#include <stdlib.h>

// 定义二叉树节点结构
typedef struct TreeNode {
    int val;                    // 节点值
    struct TreeNode *left;      // 左子树指针
    struct TreeNode *right;     // 右子树指针
} TreeNode;

// 创建新节点
TreeNode* createNode(int val) {
    TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
    node->val = val;
    node->left = NULL;
    node->right = NULL;
    return node;
}

/**
 * 查找两个节点的最近公共祖先(LCA)
 * 
 * @param root 当前子树根节点
 * @param p    目标节点1
 * @param q    目标节点2
 * @return     最近公共祖先节点指针
 */
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    // 递归终止条件:遇到空节点或目标节点
    if (root == NULL || root == p || root == q) {
        return root;
    }
    
    // 递归搜索左右子树
    TreeNode* left = lowestCommonAncestor(root->left, p, q);
    TreeNode* right = lowestCommonAncestor(root->right, p, q);
    
    /* 后序遍历结果合并 */
    // 情况1:p和q分布在左右子树 -> 当前节点是LCA
    if (left != NULL && right != NULL) {
        return root;
    }
    // 情况2:LCA位于左子树
    if (left != NULL) {
        return left;
    }
    // 情况3:LCA位于右子树
    if (right != NULL) {
        return right;
    }
    // 情况4:子树中不含目标节点
    return NULL;
}

int main() {
    /* 构建示例树结构 */
    // 创建所有节点
    TreeNode* root = createNode(3);
    TreeNode* node5 = createNode(5);
    TreeNode* node1 = createNode(1);
    TreeNode* node6 = createNode(6);
    TreeNode* node2 = createNode(2);
    TreeNode* node0 = createNode(0);
    TreeNode* node8 = createNode(8);
    TreeNode* node7 = createNode(7);
    TreeNode* node4 = createNode(4);
    
    // 构建树连接关系
    root->left = node5;
    root->right = node1;
    node5->left = node6;
    node5->right = node2;
    node2->left = node7;
    node2->right = node4;
    node1->left = node0;
    node1->right = node8;
    
    /* 测试用例1:p=5, q=1 -> LCA应为3 */
    TreeNode* lca1 = lowestCommonAncestor(root, node5, node1);
    printf("Test Case 1: p=5, q=1 -> LCA=%d\n", lca1->val);  // 应输出3
    
    /* 测试用例2:p=5, q=4 -> LCA应为5 */
    TreeNode* lca2 = lowestCommonAncestor(root, node5, node4);
    printf("Test Case 2: p=5, q=4 -> LCA=%d\n", lca2->val);  // 应输出5
    
    /* 测试用例3:p=7, q=4 -> LCA应为2 */
    TreeNode* lca3 = lowestCommonAncestor(root, node7, node4);
    printf("Test Case 3: p=7, q=4 -> LCA=%d\n", lca3->val);  // 应输出2
    
    /* 测试用例4:p=6, q=8 -> LCA应为3 */
    TreeNode* lca4 = lowestCommonAncestor(root, node6, node8);
    printf("Test Case 4: p=6, q=8 -> LCA=%d\n", lca4->val);  // 应输出3
    
    /* 释放所有节点内存 */
    free(node4); free(node7);
    free(node2); free(node6);
    free(node5); free(node0);
    free(node8); free(node1);
    free(root);
    
    return 0;
}

输出结果:


问题二:后继者(Inorder Successor)—— 二叉搜索树的秩序之链
设计一个算法,找出二叉搜索树中指定节点的“下一个”节点(也即中序后继)。如果指定节点没有对应的“下一个”节点,则返回null。

🌲 问题核心
在二叉搜索树(BST)中,求指定节点 p 的中序后继(即中序遍历序列中的下一个节点)。若 p 为末节点,返回 null

📜 示例具象化

      5
     / \
    3   6
   / \
  2   4
 /     
1
  • 示例1p=1 → 后继=2(中序序列:[1,2,3,4,5,6])。

  • 示例2p=6 → 后继=null(无后继)。

⚔️ 算法设计:BST 有序性的利刃

  1. 情形一:p 有右子树

    • 后继为 右子树的最左节点(即右子树中的最小值节点)。

    • 示例:若 p=3,后继为 4(右子树 4 无左子节点)。

  2. 情形二:p 无右子树

    • 后继为 首个大于 p 的祖先节点(需从根向下搜索)。

    • 算法步骤:

      • 初始化 successor = null

      • 从根节点遍历:

        • 若当前节点值 > p.val,则 successor = 当前节点,并向 左子树 搜索(寻找更小的候选)。

        • 若当前节点值 ≤ p.val,则向 右子树 搜索(后继不在此路径)。

🔍 复杂度剖析

  • 时间复杂度O(h)h 为树高,BST 平衡时为 O(log n))。

  • 空间复杂度O(1)(迭代法无需额外空间)。

🎯 示例推演(p=4

  1. p=4 无右子树 → 进入情形二。

  2. 从根节点 5 开始:

    • 5 > 4 → successor=5,向左子树 3 搜索。

    • 3 < 4 → 向右子树 4 搜索(命中 p,终止)。

  3. 返回 successor=5(验证中序序列 [1,2,3,4,5,6]4 的后继为 5)。

关键洞见:BST 的 有序性 将搜索路径压缩为单向路径,避免全局遍历。

题目程序:

#include <stdio.h>   // 标准输入输出头文件
#include <stdlib.h>  // 标准库头文件(包含内存分配函数)

// 定义二叉搜索树节点结构体
typedef struct TreeNode {
    int val;                    // 节点存储的整数值
    struct TreeNode *left;      // 指向左子节点的指针
    struct TreeNode *right;     // 指向右子节点的指针
} TreeNode;

// 创建新节点的函数
TreeNode* createNode(int val) {
    TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));  // 动态分配节点内存
    node->val = val;             // 设置节点值
    node->left = NULL;           // 初始化左子节点为空
    node->right = NULL;          // 初始化右子节点为空
    return node;                 // 返回新创建的节点指针
}

/**
 * 在二叉搜索树中查找指定节点的中序后继
 * 
 * @param root 二叉搜索树的根节点
 * @param p    需要查找后继的节点
 * @return     后继节点指针(若无后继则返回NULL)
 */
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
    // 情况1:如果目标节点有右子树
    if (p->right != NULL) {
        TreeNode* current = p->right;  // 从右子节点开始
        // 循环查找右子树的最左侧节点
        while (current->left != NULL) {
            current = current->left;   // 持续向左移动
        }
        return current;  // 返回右子树中的最小值节点
    }
    
    // 情况2:目标节点无右子树
    TreeNode* successor = NULL;  // 初始化后继节点为空
    TreeNode* current = root;    // 从根节点开始遍历
    
    // 遍历二叉搜索树寻找后继
    while (current != NULL) {
        if (current->val > p->val) {  // 当前节点值大于目标值
            successor = current;      // 当前节点可能是后继(候选)
            current = current->left;  // 向左子树移动(寻找更小的候选值)
        } else if (current->val < p->val) {  // 当前节点值小于目标值
            current = current->right;        // 向右子树移动(当前节点太小)
        } else {  // 找到目标节点(current == p)
            break;  // 终止遍历
        }
    }
    
    return successor;  // 返回找到的后继节点(若无则返回NULL)
}

int main() {
    /* 构建示例二叉搜索树 */
    // 创建所有节点
    TreeNode* root = createNode(5);    // 根节点
    TreeNode* node3 = createNode(3);   // 左子节点
    TreeNode* node6 = createNode(6);   // 右子节点
    TreeNode* node2 = createNode(2);   // 左子树左子节点
    TreeNode* node4 = createNode(4);   // 左子树右子节点
    TreeNode* node1 = createNode(1);   // 最左侧节点
    
    // 构建树结构连接关系
    root->left = node3;    // 5的左子节点是3
    root->right = node6;   // 5的右子节点是6
    node3->left = node2;   // 3的左子节点是2
    node3->right = node4;  // 3的右子节点是4
    node2->left = node1;   // 2的左子节点是1
    
    /* 测试用例1:p=1(值为1的节点) */
    TreeNode* succ1 = inorderSuccessor(root, node1);
    printf("Test Case 1: p=1 -> Successor=%d\n", succ1 ? succ1->val : -1);  // 预期输出2
    
    /* 测试用例2:p=2 */
    TreeNode* succ2 = inorderSuccessor(root, node2);
    printf("Test Case 2: p=2 -> Successor=%d\n", succ2 ? succ2->val : -1);  // 预期输出3
    
    /* 测试用例3:p=3 */
    TreeNode* succ3 = inorderSuccessor(root, node3);
    printf("Test Case 3: p=3 -> Successor=%d\n", succ3 ? succ3->val : -1);  // 预期输出4
    
    /* 测试用例4:p=4 */
    TreeNode* succ4 = inorderSuccessor(root, node4);
    printf("Test Case 4: p=4 -> Successor=%d\n", succ4 ? succ4->val : -1);  // 预期输出5
    
    /* 测试用例5:p=5 */
    TreeNode* succ5 = inorderSuccessor(root, root);
    printf("Test Case 5: p=5 -> Successor=%d\n", succ5 ? succ5->val : -1);  // 预期输出6
    
    /* 测试用例6:p=6(末节点,无后继) */
    TreeNode* succ6 = inorderSuccessor(root, node6);
    printf("Test Case 6: p=6 -> Successor=%s\n", succ6 ? "Exists" : "NULL");  // 预期输出NULL
    
    /* 释放所有节点内存(后序遍历顺序) */
    free(node1);  // 释放节点1
    free(node2);  // 释放节点2
    free(node4);  // 释放节点4
    free(node3);  // 释放节点3
    free(node6);  // 释放节点6
    free(root);   // 释放根节点
    
    return 0;  // 程序正常退出
}

输出结果:


双雄对决:LCA vs 后继者的算法博弈论
维度首个共同先祖(LCA)后继者(Inorder Successor)
适用树型任意二叉树二叉搜索树(BST)
核心约束禁止额外存储节点无特殊约束
算法范式递归分治(后序遍历)迭代搜索(利用有序性)
时间复杂度O(n)(遍历整树)O(h)(树高决定)
空间复杂度O(h)(递归栈)O(1)(迭代法)
关键操作子树结果合并右子树最左节点 / 祖先路径搜索
实战场景家谱关系分析、编译器语法树解析数据库索引遍历、有序迭代器实现

🔄 对比图示

         LCA 算法                         后继者算法
          │                                │
          ▼                                ▼
    [递归深入子树]                    [判断右子树存否]
          │                                │
    [自底向上回溯]─┬─→[左右非空→当前为LCA]   ├─→[有右子树→取最左节点]
          │       ├─→[左非空→返回左]        │
          │       └─→[右非空→返回右]        └─→[无右子树→祖先搜索]
          │                                │
          ▼                                ▼
    [合并状态定LCA]                    [返回后继节点]

终极总结:拓扑与秩序的辩证统一
  1. LCA 的递归美学

    • 以 后序遍历 为剑,化整为零,通过子树结果的合并重构全局拓扑。

    • 适用性广,但受限于 O(n) 时间复杂度,在巨型树中可能成为瓶颈。

  2. 后继者的有序之力

    • 借 BST 有序性 为弓,将问题压缩为单向路径搜索,效率跃升至 O(h)

    • 极度依赖树的有序结构,在普通二叉树中需退化为 O(n) 的中序遍历。

  3. 哲学启示

    • LCA 是拓扑的:它解构节点间的空间关系,揭示树结构的连通性。

    • 后继者是时序的:它维护中序遍历的线性秩序,体现BST的动态连续性。

    • 二者殊途同归:皆以最小代价在非线性结构中提取线性信息,彰显算法设计中对 “分治” 与 “有序” 的极致利用!

二叉树的江湖,从无绝对的胜负。LCA 以递归之刃劈开血脉迷雾,后继者借有序之翼穿越秩序长河。当你下次面对千层枝桠,愿你能如林间智者般低语:“拓扑与秩序,不过是一枚硬币的两面”。

思考题:若将 LCA 问题应用于 BST,能否结合有序性优化至 O(h)
彩蛋:在红黑树中,后继者的操作如何维持 O(log n) 的永恒优雅?

(全文完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

司铭鸿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值