题目:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例2
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
这道题做的我一波三折,折腾了一个上午。看到题目后受前几天做题的影响,就想用先中后序遍历之类的方式,思考了一阵后想到的解决方案是利用层序遍历获得每个节点的下标,然后通过下标来获得树节点往上遍历的路径(除2就完事了),寻找到第一个交叉点就好了。最开始想着时间复杂度是O(N),应该还能行(这里没想到应该只考虑非null节点数时间复杂度是真的蠢,也是树的题目做的太少了),然后折腾了一个上午写好了层序遍历(甚至把另外一道层序遍历题做了)和这个题的题解,结果测试的时候TLE了,看着测试用例里边满屏的null陷入了沉思。
心态有点小蹦,后面想既然这样的话修改应该是只用遍历非空节点,获得路径的思想没有问题,就是遍历的方式错了。突然,脑力里边灵光乍现Tarjan ,我以前不是在书上看过这种题型吗??还折腾了这么久,自动浮现coding一定要笑.jpg。
到这已经没心情复现了,稿纸上写了下伪代码就去看题解了,其实官方题解第二个就是Tarjan ,主要思想也就进行dfs存储路径然后寻找最近的交叉点,详情可见https://www.cnblogs.com/JVxie/p/4854719.html,官方实现的代码也挺简洁的,只是没想到Tarjan会比递归的方式更慢
class Solution {
public:
unordered_map<int, TreeNode*> fa;
unordered_map<int, bool> vis;
void dfs(TreeNode* root){
if (root->left != nullptr) {
fa[root->left->val] = root;
dfs(root->left);
}
if (root->right != nullptr) {
fa[root->right->val] = root;
dfs(root->right);
}
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
fa[root->val] = nullptr;
dfs(root);
while (p != nullptr) {
vis[p->val] = true;
p = fa[p->val];
}
while (q != nullptr) {
if (vis[q->val]) return q;
q = fa[q->val];
}
return nullptr;
}
};
执行结果
递归最主要就是就是找到相关规律来确定return,我一开始嫌弃递归慢所以都没往这方面想,果然还是太菜了。规律主要是左右都有p或者q(一边一个,本身也可以算做左右),那就是最终返回的结果;左没便在右,右没便在左。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) {
return root;
}
if (root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) {
return root;
} else if (left != null) {
return left;
} else if (right != null) {
return right;
}
return null;
}
}
执行结果