「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」
题目
面试题 04.08. 首个共同祖先
设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索树。
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
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。因为根据定义最近公共祖先节点可以为节点本身。
解法
二叉树的后序遍历。
思路
使用二叉树的后序遍历,去左右子树里找p和q节点。
p或q的分布,有三种可能
- p和q在子树两侧
- p和q在同侧
对于根节点root来说,那么会有三种情况发生。
- 在左右子树里分别找到了p和q
- 在左子树或右子树里找到了p和q
- 根节点即为p或q,那就不用继续找了,根节点就是最近的祖先。
我们再用递归的眼光去看这个问题,假设是一个完整二叉树,好理解一点。对于根节点,我们会有如下判断和操作
- 如果当前根节点不是p或者q,那么会一直找下去,直到找到左叶子节点。
- 由于左叶子节点的左右子节点都是null,没办法找了,那么我们就返回null,代表在当前这个叶子节点没有找到p或q。
- 然后我们去找右叶子节点,如果也没找到,同理,也返回null。
- 这个时候就代表对于左叶子节点和右叶子节点的根节点,在它的子树里没有找到p或者q,所以它也返回null。
所以我们知道,在遍历过程中,在某个节点的子树里如果没有找到p或者q,那么就返回null,如果找到了p或q,那么返回的就是p或者q节点。一旦在某个节点的子树找到了,对于该节点的所有祖先节点来说,返回值都不会是null。
另外还有一点,如果在某一个节点找到了p或q,其实遍历也没必要往下继续,因为既然能够到这个节点来,那么它的祖先都不是p或者q,那么p、q中的另一个一定在它的子树,或者对于它的父节点所在的子树的另一侧子树中。
代码
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function (root, p, q) {
// 后序遍历递归该树
return postOrder(root,p,q);
};
function postOrder(root, p, q) {
if(root == null){
return null;
}
// 如果当前节点就是p或q,则直接返回当前节点
if(root == p || root == q){
return root;
}
// 去左子树里找,一直找到叶子节点,如果找到了,则该left代表找到的那个p或q
let left = postOrder(root.left,p,q);
// 同理,去右子树里找
let right = postOrder(root.right,p,q);
// 以下分4中情况
// 1.左右都找到了p或q,那么当前节点就是最近祖先
if(left != null && right != null){
return root;
// 2. 左子树找到了,右子树没找到,则目前来说,该节点是最近祖先
}else if(left != null){
return left;
// 3. 右子树找到了,左子树没找到,则目前来说,该节点是最近祖先
}else if(right !=null){
return right;
// 两侧都没找到,说明该节点子树未找到,则返回null
}else{
return null;
}
}
复杂度分析
时间复杂度:O(n),最坏情况要将所有节点都遍历一遍。
空间复杂度:O(n),最坏情况二叉树为链表,且两个节点在最末端,那么需要将所有节点遍历一遍,空间复杂度为二叉树的高度,即为n。