算法练习(14)-二叉树中2个节点的最近公共祖先?_子树

比如这颗树,给定2个节点: 4、5 ,它们的最近公共祖先节点为2。类似的,如果是3、5,它们的最近公共祖先节点为1。

一种比较容易想到的思路,如果知道每个节点到root的全路径, 比如

3到root节点的全路径为: 3->1

5到root节点的全路径为: 5->2->1

这样,只要遍历对比下全路径, 就能知道最近的公共节点是1,求每个节点到根节点全路径的方法,在以前的文章算法练习(11)-二叉树的各种遍历 有详细代码,此处直接复用即可。



/**
* 获取每个节点到根节点的全路径
*
* @param node
* @return
*/
public static Map<TreeNode, List<TreeNode>> getToRootPath(TreeNode node) {
if (node == null) {
return null;
}
//记录每个节点->父节点的1:1映射
Map<TreeNode, TreeNode> parentMap = new HashMap<>();

Queue<TreeNode> queue = new LinkedList<>();
queue.add(node);
parentMap.put(node, null);
while (!queue.isEmpty()) {
TreeNode n = queue.poll();
if (n.left != null) {
queue.add(n.left);
parentMap.put(n.left, n);
}
if (n.right != null) {
queue.add(n.right);
parentMap.put(n.right, n);
}
}

//根据parentMap,整理出完整的到根节点的全路径
Map<TreeNode, List<TreeNode>> result = new HashMap<>();
for (Map.Entry<TreeNode, TreeNode> entry : parentMap.entrySet()) {
TreeNode self = entry.getKey();
TreeNode parent = entry.getValue();

//把当前节点,先保护起来
TreeNode temp = self;
List<TreeNode> path = new ArrayList<>();
while (parent != null) {
path.add(self);
self = parent;
parent = parentMap.get(self);
if (parent == null) {
path.add(self);
}
}
result.put(temp, path);
}
return result;
}


/**
* 求节点n1,n2的最近公共祖先
* @param root
* @param n1
* @param n2
* @return
*/
public static TreeNode getNearestSameParentNode(TreeNode root, TreeNode n1, TreeNode n2) {
if (n1 == null || n2 == null) {
return n1 == null ? n2 : n1;
}
if (n1.equals(n2)) {
return n1;
}

Map<TreeNode, List<TreeNode>> path = getToRootPath(root);
List<TreeNode> n1Path = path.get(n1);
List<TreeNode> n2Path = path.get(n2);
for (TreeNode node1 : n1Path) {
for (TreeNode node2 : n2Path) {
if (node1.equals(node2)) {
return node1;
}
}
}

return null;
}

public static void main(String[] args) {

TreeNode n1 = new TreeNode(1);
TreeNode n2_1 = new TreeNode(2);
TreeNode n2_2 = new TreeNode(3);
TreeNode n3_1 = new TreeNode(4);
TreeNode n3_2 = new TreeNode(5);
TreeNode n4_1 = new TreeNode(6);
TreeNode n4_2 = new TreeNode(7);

n1.left = n2_1;
n1.right = n2_2;

n2_1.left = n3_1;
n2_1.right = n3_2;

n2_2.left = n4_1;
n2_2.right = n4_2;

TreeNode result1 = getNearestSameParentNode(n1, n4_1, n4_2);
System.out.println(n4_1 + "/" + n4_2 + " : " + result1);

TreeNode result2 = getNearestSameParentNode(n1, n2_2, n3_2);
System.out.println(n2_2 + "/" + n3_2 + " : " + result2);

TreeNode result3 = getNearestSameParentNode(n1, n2_1, n3_1);
System.out.println(n2_1 + "/" + n3_1 + " : " + result3);
}


输出:



6/7 : 3
3/5 : 1
2/4 : 2


这个方法,虽然思路很容易理解 ,但效率并不高, 额外空间借助了2个Map,空间复杂度至少也是O(N)级别,然后再做2轮循环比较全路径,时间复杂度也可以优化,事实上有更好的解法:



    public static TreeNode getNearestSameParentNode(TreeNode root, TreeNode n1, TreeNode n2) {
if (root == null || root == n1 || root == n2) {
return root;
}

TreeNode left = getNearestSameParentNode(root.left, n1, n2);
TreeNode right = getNearestSameParentNode(root.right, n1, n2);

if (left != null && right != null) {
return root;
}

return left != null ? left : right;
}


这个代码很短, 但不太好理解 , 先分析下一颗树中的2个节点X、Y,它们最近公共祖先的情况:

算法练习(14)-二叉树中2个节点的最近公共祖先?_algorithm_02

只会出现这2类情况:

1、节点X在Y的某1侧子树中(反过来也一样, Y出现在X的某1侧子树中),即:1个节点就是另1个的最近公共祖先。

2、节点X与Y,必须向上汇聚, 才能走到1个最近的交叉节点

在优化版的代码中,使用了递归求解。

第1段if判断,其实有2层意思,用于处理递归过程中的边界返回:



        if (root == null || //递归到最末端叶节点时的函数返回
(root == n1 || root == n2) //在左右子树遍历过程中,如果发现当前节点就是n1或n2,直接返回,因为下面的子节点,肯定不可能再是它俩的公共祖先节点了
) {
return root;
}


最后1段return,表示遍历过程中,如果X或Y只出现在左子树中或右子树中,哪边出现就返回哪边,相当于下图这种情况,返回的是X,另1侧的返回null被扔掉.



return left != null ? left : right;


算法练习(14)-二叉树中2个节点的最近公共祖先?_最近公共祖先_03

中间一段if,则表示是情况2:



        if (left != null && right != null) {
return root;
}


在遍历的过程中,如果X与Y出现在某1个节点的2侧,最后左、右子树的遍历中, 会把它俩都返回过来,出现这种情况,说明X与Y汇聚于当前节点,这个节点就是最近的公共祖先。


作者:菩提树下的杨过