在算法的星河中,二叉树如同枝繁叶茂的智慧之树,每一道题目都是其上的璀璨果实。今日,我们将剖开两颗尤为耀眼的果实——首个共同先祖(LCA) 与 后继者(Inorder Successor)。前者是血脉相连的寻根之旅,后者是孤舟独行的秩序之问。无需代码,仅凭逻辑与图示,随我穿越递归的迷雾,解构分治的兵法,揭开二叉树最精妙的拓扑博弈!
问题一:首个共同先祖(LCA)—— 二叉树的血脉寻踪
设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索
🌳 问题核心
给定任意二叉树(非二叉搜索树)和两个节点 p
与 q
,求其最近公共祖先(LCA)。关键约束:禁止额外存储节点,仅靠递归栈空间破局。
📜 示例具象化
3 / \ 5 1 / \ / \ 6 2 0 8 / \ 7 4
-
示例1:
p=5, q=1
→ LCA=3
(分属左右子树) -
示例2:
p=5, q=4
→ LCA=5
(节点自身是祖先)
⚔️ 算法设计:递归分治的拓扑兵法
-
递归基:若当前节点为
null
、p
或q
,直接返回该节点。 -
左右分治:
-
左子树递归搜索
p
和q
,结果记为left
。 -
右子树递归搜索
p
和q
,结果记为right
。
-
-
状态合并(后序遍历精髓):
-
left
与right
均非空 → 当前节点为 LCA(p
和q
分居左右)。 -
left
非空,right
为空 → 返回left
(LCA 在左子树)。 -
right
非空,left
为空 → 返回right
(LCA 在右子树)。
-
🔍 复杂度剖析
-
时间复杂度:
O(n)
(最坏遍历所有节点)。 -
空间复杂度:
O(h)
(递归栈深度,h
为树高)。
🎯 示例推演(p=5, q=4
)
-
从根节点
3
出发,递归左子树(5
)和右子树(1
)。 -
左子树
5
:-
左子树
6
返回null
(无p
或q
)。 -
右子树
2
:递归其左子树7
(null
)、右子树4
(命中q
),返回4
。 -
节点
2
合并结果:left=null
,right=4
→ 返回4
。
-
-
节点
5
合并结果:left=null
(6
的返回),right=4
(2
的返回)→5
自身是p
,返回5
。 -
根节点
3
:left=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
-
示例1:
p=1
→ 后继=2
(中序序列:[1,2,3,4,5,6]
)。 -
示例2:
p=6
→ 后继=null
(无后继)。
⚔️ 算法设计:BST 有序性的利刃
-
情形一:
p
有右子树-
后继为 右子树的最左节点(即右子树中的最小值节点)。
-
示例:若
p=3
,后继为4
(右子树4
无左子节点)。
-
-
情形二:
p
无右子树-
后继为 首个大于
p
的祖先节点(需从根向下搜索)。 -
算法步骤:
-
初始化
successor = null
。 -
从根节点遍历:
-
若当前节点值
> p.val
,则successor = 当前节点
,并向 左子树 搜索(寻找更小的候选)。 -
若当前节点值
≤ p.val
,则向 右子树 搜索(后继不在此路径)。
-
-
-
🔍 复杂度剖析
-
时间复杂度:
O(h)
(h
为树高,BST 平衡时为O(log n)
)。 -
空间复杂度:
O(1)
(迭代法无需额外空间)。
🎯 示例推演(p=4
)
-
p=4
无右子树 → 进入情形二。 -
从根节点
5
开始:-
5 > 4
→successor=5
,向左子树3
搜索。 -
3 < 4
→ 向右子树4
搜索(命中p
,终止)。
-
-
返回
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] [返回后继节点]
终极总结:拓扑与秩序的辩证统一
-
LCA 的递归美学:
-
以 后序遍历 为剑,化整为零,通过子树结果的合并重构全局拓扑。
-
适用性广,但受限于
O(n)
时间复杂度,在巨型树中可能成为瓶颈。
-
-
后继者的有序之力:
-
借 BST 有序性 为弓,将问题压缩为单向路径搜索,效率跃升至
O(h)
。 -
极度依赖树的有序结构,在普通二叉树中需退化为
O(n)
的中序遍历。
-
-
哲学启示:
-
LCA 是拓扑的:它解构节点间的空间关系,揭示树结构的连通性。
-
后继者是时序的:它维护中序遍历的线性秩序,体现BST的动态连续性。
-
二者殊途同归:皆以最小代价在非线性结构中提取线性信息,彰显算法设计中对 “分治” 与 “有序” 的极致利用!
-
二叉树的江湖,从无绝对的胜负。LCA 以递归之刃劈开血脉迷雾,后继者借有序之翼穿越秩序长河。当你下次面对千层枝桠,愿你能如林间智者般低语:“拓扑与秩序,不过是一枚硬币的两面”。
思考题:若将 LCA 问题应用于 BST,能否结合有序性优化至
O(h)
?
彩蛋:在红黑树中,后继者的操作如何维持O(log n)
的永恒优雅?
(全文完)