大数跨境
0
0

二叉树的最近公共祖先:五种语言实现与深度解析

二叉树的最近公共祖先:五种语言实现与深度解析 点点技术屋
2025-12-09
0

问题定义与核心挑战

2025年12月9日,LeetCode Hot 100榜单中,"二叉树的最近公共祖先"一题以73.9%的通过率位居中等难度题目前列。这个看似简单的问题,却蕴含着二叉树遍历与递归思想的精髓。给定一棵二叉树的根节点root,以及两个指定节点p和q,我们需要找到这两个节点的最近公共祖先(LCA)。所谓最近公共祖先,是指在二叉树中同时拥有p和q作为后代的最深节点(这里允许节点成为其自身的祖先)。

问题的三个关键特性

  1. 公共祖先的定义:节点x是p和q的公共祖先,当且仅当x是p的祖先且x是q的祖先

  2. 最近的判定标准:在所有公共祖先中深度最大的那个节点

  3. 特殊情况处理:当p是q的祖先时,p就是最近公共祖先;反之亦然

递归解法:从底向上的智慧

算法核心思想

递归解法的精妙之处在于它采用后序遍历的方式,从叶子节点开始向上回溯。这种"自底向上"的策略能让我们在第一次遇到同时包含p和q的节点时,立即确定它就是最近公共祖先。

递归函数的返回值含义

  • 如果当前节点的左子树包含p或q,返回左子树中找到的节点

  • 如果当前节点的右子树包含p或q,返回右子树中找到的节点

  • 如果当前节点本身就是p或q,返回当前节点

  • 如果左右子树分别返回非空节点,说明当前节点就是LCA,返回当前节点

  • 如果左右子树都返回空,返回空

五种语言实现代码

Java实现

class TreeNode {    int val;    TreeNode left;    TreeNode right;    TreeNode(int x) { val = x; }}
class Solution {    public 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);
        // 左右子树都找到,当前节点是LCA        if (left != null && right != null) {            return root;        }        // 只在左子树找到        else if (left != null) {            return left;        }        // 只在右子树找到        else {            return right;        }    }}

Python实现

class TreeNode:    def __init__(self, x):        self.val = x        self.left = None        self.right = None
class Solution:    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':        # 递归终止条件        if not root or root == p or root == q:            return root
        # 后序遍历        left = self.lowestCommonAncestor(root.left, p, q)        right = self.lowestCommonAncestor(root.right, p, q)
        # 左右子树都找到,当前节点是LCA        if left and right:            return root        # 只在左子树找到        elif left:            return left        # 只在右子树找到        else:            return right

Go实现

type TreeNode struct {    Val   int    Left  *TreeNode    Right *TreeNode}
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {    // 递归终止条件    if root == nil || root == p || root == q {        return root    }
    // 后序遍历    left := lowestCommonAncestor(root.Left, p, q)    right := lowestCommonAncestor(root.Right, p, q)
    // 左右子树都找到,当前节点是LCA    if left != nil && right != nil {        return root    }    // 只在左子树找到    if left != nil {        return left    }    // 只在右子树找到    return right}

Rust实现

// Definition for a binary tree node.#[derive(DebugPartialEqEq)]pub struct TreeNode {  pub val: i32,  pub left: Option<Rc<RefCell<TreeNode>>>,  pub right: Option<Rc<RefCell<TreeNode>>>,}
impl TreeNode {  #[inline]  pub fn new(val: i32) -> Self {    TreeNode {      val,      left: None,      right: None    }  }}
use std::rc::Rc;use std::cell::RefCell;
impl Solution {    pub fn lowest_common_ancestor(root: Option<Rc<RefCell<TreeNode>>>, p: Option<Rc<RefCell<TreeNode>>>, q: Option<Rc<RefCell<TreeNode>>>) -> Option<Rc<RefCell<TreeNode>>> {        // 处理空节点情况        if root.is_none() {            return None;        }
        // 检查当前节点是否是p或q        if root == p || root == q {            return root;        }
        let root_clone = root.clone();        let left = Self::lowest_common_ancestor(            root_clone.as_ref().unwrap().borrow().left.clone(),            p.clone(),            q.clone()        );
        let right = Self::lowest_common_ancestor(            root.as_ref().unwrap().borrow().right.clone(),            p,            q        );
        // 左右子树都找到,当前节点是LCA        if left.is_some() && right.is_some() {            return root;        }        // 只在左子树找到        else if left.is_some() {            return left;        }        // 只在右子树找到        else {            return right;        }    }}

C++实现

struct TreeNode {    int val;    TreeNode *left;    TreeNode *right;    TreeNode(int x) : val(x), left(NULL), right(NULL) {}};
class Solution {public:    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {        // 递归终止条件        if (root == nullptr || root == p || root == q) {            return root;        }
        // 后序遍历        TreeNode* left = lowestCommonAncestor(root->left, p, q);        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        // 左右子树都找到,当前节点是LCA        if (left != nullptr && right != nullptr) {            return root;        }        // 只在左子树找到        else if (left != nullptr) {            return left;        }        // 只在右子树找到        else {            return right;        }    }};

算法流程与示例分析

典型案例分析

让我们通过一个具体示例来理解算法的执行过程。考虑如下二叉树,其中p=7,q=8:

算法执行步骤:

  1. 从根节点3开始递归,分别遍历左子树(5)和右子树(1)

  2. 在左子树5中继续递归,遍历左子树(6)和右子树(2)

  3. 节点6的左右子树均为空,返回空

  4. 节点2的左子树(7)找到p节点,返回7;右子树(4)返回空,因此节点2返回7

  5. 节点5的左子树返回空,右子树返回7,因此节点5返回7

  6. 在右子树1中递归,遍历左子树(0)和右子树(8)

  7. 节点0返回空,节点8找到q节点,返回8,因此节点1返回8

  8. 根节点3的左子树返回7,右子树返回8,因此根节点3是LCA,返回3

特殊情况处理

算法优雅地处理了多种特殊情况:

  1. 一个节点是另一个节点的祖先:当p是q的祖先时,算法会在遍历到p节点时直接返回p,因为此时p的子树中包含q,而p本身就是最深的公共祖先

  2. 一个节点就是根节点:当p或q中有一个是根节点时,根节点自然就是最近公共祖先

  3. 树退化为链表:即使二叉树退化为单链表结构,算法依然能正确找到LCA,此时时间复杂度仍为O(n)

复杂度分析与优化

时间复杂度

递归解法的时间复杂度为O(n),其中n是二叉树的节点数。在最坏情况下(如退化为链表的二叉树),我们需要访问树中的每个节点一次。

空间复杂度

空间复杂度为O(h),其中h是二叉树的高度。这是因为递归调用栈的深度取决于树的高度。在平衡二叉树中,h=log n;在最坏情况下(退化为链表),h=n。

非递归解法思路

虽然递归解法简洁优雅,但对于深度较大的二叉树可能导致栈溢出。非递归解法通常采用以下两种思路:

  1. 路径记录法:分别记录从根节点到p和q的路径,然后找出两条路径的最后一个公共节点

  2. 后序遍历迭代法:使用栈模拟后序遍历过程,记录每个节点的访问状态

路径记录法的实现需要额外O(n)的空间来存储路径,但可以有效避免栈溢出问题,适合处理大型二叉树。

实际应用与扩展

二叉搜索树中的LCA

如果题目中的二叉树是二叉搜索树(BST),我们可以利用BST的特性(左子树所有节点值小于根节点,右子树所有节点值大于根节点)来优化算法:

// BST中的LCA优化算法public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {    // 如果当前节点值大于p和q的值,LCA在左子树    if (root.val > p.val && root.val > q.val) {        return lowestCommonAncestor(root.left, p, q);    }    // 如果当前节点值小于p和q的值,LCA在右子树    else if (root.val < p.val && root.val < q.val) {        return lowestCommonAncestor(root.right, p, q);    }    // 否则当前节点就是LCA    else {        return root;    }}

这种方法的时间复杂度仍为O(n),但在平衡BST中平均情况下可以达到O(log n)。

多叉树的LCA问题

在多叉树中寻找LCA需要对算法进行扩展,通常采用以下策略:

  1. 记录每个节点的父节点,然后使用类似链表相交的方法寻找LCA

  2. 扩展后序遍历,对每个节点统计其子树中包含的目标节点数量

实际应用场景

LCA问题在实际应用中非常广泛:

  1. 家谱树分析:在家族树中寻找两个人的最近共同祖先

  2. 编译器设计:在抽象语法树(AST)中确定两个节点的作用域关系

  3. 版本控制系统:Git等版本控制系统使用LCA算法寻找两个分支的最近共同祖先版本

  4. 生物信息学:在进化树中确定两个物种的最近共同祖先

总结与思考

"二叉树的最近公共祖先"问题虽然简单,却很好地体现了二叉树遍历和递归思想的精髓。通过五种语言的实现对比,我们可以看到不同编程语言在处理递归和指针/引用时的特点:

  • Java和C++使用显式的指针/引用

  • Python和Go通过简洁的语法简化了递归实现

  • Rust的所有权模型要求更细致的引用管理

这一问题的解决思路可以推广到许多类似的树结构问题中,如"二叉树的直径"、"二叉树中的最大路径和"等。掌握LCA问题的解法,将为处理更复杂的树结构问题打下坚实基础。

在实际面试中,面试官往往会从基础的LCA问题出发,逐步深入到更复杂的扩展场景,如要求处理带父指针的二叉树、多叉树,或限制空间复杂度等。因此,我们不仅要记住算法实现,更要理解其背后的思想本质,才能灵活应对各种变化。

最后,值得一提的是,LCA问题的解法中蕴含着分治思想的火花——将复杂问题分解为简单子问题,通过解决子问题来构建原问题的解。这种思想在算法设计中具有普遍意义,是每个程序员都应该掌握的核心思维方法。

【声明】内容源于网络
0
0
点点技术屋
写自己擅长的Nginx,Linux,Redis,JVM和Golang 写自己学习的算法,运维总结 写一些散文随笔 写我最爱的大熊猫,发大熊猫的图片
内容 120
粉丝 0
点点技术屋 写自己擅长的Nginx,Linux,Redis,JVM和Golang 写自己学习的算法,运维总结 写一些散文随笔 写我最爱的大熊猫,发大熊猫的图片
总阅读4
粉丝0
内容120