剑指 Offer(第2版)面试题 68:树中两个结点的最低公共祖先
- 剑指 Offer(第2版)面试题 68:树中两个结点的最低公共祖先
- 解法1:递归
- 拓展题:二叉搜索树的最近公共祖先
- 解法1:两次遍历
- 解法2:一次遍历
剑指 Offer(第2版)面试题 68:树中两个结点的最低公共祖先
题目来源:88. 树中两个结点的最低公共祖先
题目描述:给出一个二叉树,输入两个树节点,求它们的最低公共祖先。一个树节点的祖先节点包括它本身。
解法1:递归
祖先的定义: 若节点 p 在节点 root 的左(右)子树中,或 p=root,则称 root 是 p 的祖先。
最近公共祖先的定义: 设节点 root 为节点 p、q 的某公共祖先,若其左子节点 root.left 和右子节点 root.right 都不是 p、q 的公共祖先,则称 root 是“最近的公共祖先”。
根据以上定义,若 root 是 p、q 的 最近公共祖先 ,则只可能为以下情况之一:
- p 和 q 在 root 的子树中,且分列 root 的异侧(即分别在左、右子树中);
- p=root,且 qqq 在 root 的左或右子树中;
- q=root,且 ppp 在 root 的左或右子树中;
考虑通过递归对二叉树进行先序遍历,当遇到节点 p 或 q 时返回。从底至顶回溯,当节点 p、q 在节点 root 的异侧时,节点 root 即为最近公共祖先,则向上返回 root。
代码:
/*** Definition for a binary tree node.* 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)return root;// p 和 q 其中有一个正好是 root,直接返回 root 就行if (root == p || root == q)return root;// 通过递归,得到左右两棵子树的值TreeNode *leftLCA = lowestCommonAncestor(root->left, p, q);TreeNode *rightLCA = lowestCommonAncestor(root->right, p, q);// p 和 q 分别在 root 的不同子树,直接返回 root 就行if (leftLCA && rightLCA)return root;// p 和 q 在 root 的同一侧,且 root 不等于 p 或者 q 的任何一个,那么就找 p 和 q 在的那一侧子树return leftLCA == nullptr ? rightLCA : leftLCA;}
};
复杂度分析:
时间复杂度:O(n),其中 n 是二叉树的节点个数。最差情况下,需要递归遍历树的所有节点。
空间复杂度:O(height),其中 height 是二叉树的深度。
拓展题:二叉搜索树的最近公共祖先
题目链接:235. 二叉搜索树的最近公共祖先
解法1:两次遍历
分别找到 root 到 p 和 root 到 p 的路径,因为 root 到 p 和 q 的最近公共祖先的路径长度一样,所以比较 path_p[i] 和 path_q[i] 即可,相等就说明找到了 p 和 q 的最近公共祖先。
代码:
// 两次遍历class Solution
{
public:// 主函数TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q){TreeNode *ancestor = nullptr;vector<TreeNode *> path_p = getPath(root, p);vector<TreeNode *> path_q = getPath(root, q);for (int i = 0; i < path_p.size() && i < path_q.size(); i++){if (path_p[i] == path_q[i])ancestor = path_p[i];elsebreak;}return ancestor;}// 辅函数 - 得到从根节点到目标节点的路径vector<TreeNode *> getPath(TreeNode *root, TreeNode *target){vector<TreeNode *> path;TreeNode *node = root;while (node != target){path.push_back(node);if (node->val > target->val)node = node->left;elsenode = node->right;}path.push_back(node);return path;}
};
复杂度分析:
时间复杂度:O(n),其中 n 是给定的二叉搜索树中的节点个数。
空间复杂度:O(n),其中 n 是给定的二叉搜索树中的节点个数。我们需要存储根节点到 p 和 q 的路径。
解法2:一次遍历
利用二叉搜索树的特性,只需要一次遍历。
我们从根节点开始遍历:
- 如果当前节点的值大于 p 和 q 的值,说明 p 和 q 应该在当前节点的左子树,因此将当前节点移动到它的左子节点;
- 如果当前节点的值小于 p 和 q 的值,说明 p 和 q 应该在当前节点的右子树,因此将当前节点移动到它的右子节点;
- 如果当前节点的值不满足上述两条要求,那么说明当前节点就是「分岔点」。此时,p 和 q 要么在当前节点的不同的子树中,要么其中一个就是当前节点。
代码:
// 一次遍历class Solution
{
public:TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q){TreeNode *ancestor = root;while (1){// 如果当前节点的值大于 p 和 q 的值,说明 p 和 q 在当前节点的左子树if (ancestor->val > p->val && ancestor->val > q->val)ancestor = ancestor->left;// 如果当前节点的值小于p和q的值,说明p和q在当前节点的右子树else if (ancestor->val < p->val && ancestor->val < q->val)ancestor = ancestor->right;else // 当前节点就是「分岔点」break;}return ancestor;}
};
复杂度分析:
时间复杂度:O(n),其中 n 是给定的二叉搜索树中的节点个数。
空间复杂度:O(1)。