题目地址:力扣
假设我们的根节点是root,要找的两个节点分别为p和q。从题目给的例子中可以分析最近公共祖先的两种情况:
1、当前节点既不是p也不是q,但是其左子树和右子树分别包含着p和q,那么当前节点就是p和q的最近公共祖先
2、当前节点可能是p或者q的一个,而其左子树或者右子树包含着p或q的另一个,那么当前节点就应该是p和q的最近公共祖先
由于我们判断的是左子树或右子树是否包含p或q,因此在第一种情况并非一定要祖先的左右节点正好是p,q,而是祖先的左子树中包含p,右子树中包含q即可。因此这里就涉及到状态传递,也就是只要左子树中某个节点是p,那么我们就要将这个信息传给其父节点,并一直往上传。由于我们需要先判断左子树和右子树的情况,再往上传,因此很自然就想到了左->右->根的遍历思路,所以我们需要采用后序遍历。而子树是否包含p或q,我们可以用一个布尔值来进行判断。
解法1:后序遍历递归
class Solution {
public:
bool postOrder(TreeNode* root, TreeNode* p, TreeNode* q) {
// 首先初始化当前节点的状态为false,代表当前节点既不是p也不是q
bool curflag = false;
// 若根节点非空
if (root != nullptr) {
// 若根节点是p或者根节点是q的话,将状态置为true
if (root == p || root == q)
curflag = 1;
// 递归遍历左子树和右子树
bool lflag = postOrder(root->left, p, q);
bool rflag = postOrder(root->right, p, q);
// 若左子树和右子树其中有一者是p或q
if (lflag || rflag ) {
// 若当前节点是p或q,说明对应第二种情况,即最近祖先就是当前节点
if (curflag) {
resptr = root;
// 若当前节点不是p也不是q,判断是否左右子树二者分别包含p,q
} else {
// 对应第一种情况,当前节点不是,但左右子树是,最近祖先就是当前节点
if (lflag && rflag )
resptr = root;
}
// 由于左右子树至少一者是,因此将状态传递给当前节点
curflag = true;
}
}
return curflag;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 后序遍历
postOrder(root, p, q);
return resptr;
}
private:
// 用于存储最近祖先节点的指针
TreeNode* resptr;
};
解法2:后序遍历递归(提前终止)
思路:我们换一个角度来看这个问题,由于题目中说p和q一定是两个存在的且在树中的节点。假设我们从root开始找,而且我们只关注与root与它的左右孩子。
- 假设其左孩子是p,右孩子是空,那么说明q一定在其左孩子的子树里面,因此p一定是q的祖先,p也是二者的最近祖先
- 假设其左孩子是p,右孩子是q,那么说明当前节点一定是p和q的最近祖先
其核心思想就是,对于某个节点(假设为curNode)来说,假设我找到了一个左孩子或者右孩子是p或者q,那么我就不需要继续往下递归找我的子树了,我只需要往上返回这个节点(假设为findNode)即可,让上层节点(curNode的父节点)判断它的右子树情况,若上层节点的右子树没找到p或者q,说明剩下一个一定在findNode的子树里面。若上层节点的右子树找到了p或者q,说明上层节点就是最近祖先。
因此,我们递归的时候只要找到了p或者q的一者,那么最近祖先要么就是当前找到的这一者,要么就是这一者的父节点,没有例外。具体是这一者还是其父节点,要交给其父节点来判断。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 碰到这三种情况返回,要么空,要么是p,要么是q,否则继续递归
if (!root || root == p || root == q)
return root;
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
// 交给父节点来判断,若左右子树分别包含了p和q,那么父节点就是最近祖先
if (left && right)
return root;
// 若左右子树一者包含p或q,那么就祖先节点就是那一者
return left ? left : right;
}
};
解法3:反向构树
思路:既然要求公共最近祖先,而p和q是树中的某一个节点。我只要p和q都往根节点走,那么他们一定会在某个节点相遇或者从某个节点开始走同一条路。但是树的结构又不支持往根节点走,因此我们可以通过遍历树,存下每个节点的父节点,相当于反向构建一棵树。这样我们就可以使用这个方法了。
class Solution {
public:
// 先序遍历
void preOrder(TreeNode* root, unordered_map<TreeNode*, TreeNode*> &fa_map)
{
if (root != nullptr)
{
if (root->left != nullptr)
fa_map[root->left] = root;
if (root->right != nullptr)
fa_map[root->right] = root;
preOrder(root->left, fa_map);
preOrder(root->right, fa_map);
}
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// fa_map用于存储父节点,node_set用于存储找根的时候经历的节点
unordered_map<TreeNode*, TreeNode*> fa_map;
unordered_set<TreeNode*> node_set;
// 定义res节点用于返回
TreeNode *res = nullptr;
// 构建反向树
fa_map[root] = nullptr;
preOrder(root, fa_map);
// 先从p找到根,把经历的节点加入node_set
while (p != nullptr)
{
node_set.insert(p);
p = fa_map[p];
}
// 从q往根找,只要碰到了经历过的节点,那么这个节点就是最近祖先
while(q != nullptr)
{
if (node_set.find(q) != node_set.end()) {
res = q;
break;
}
q = fa_map[q];
}
return res;
}
};
Accepted
- 31/31 cases passed (12 ms)
- Your runtime beats 93.61 % of cpp submissions
- Your memory usage beats 69.52 % of cpp submissions (13.8 MB)