算法练习(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,它们最近公共祖先的情况:

只会出现这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;

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

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

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

posted @ 2021-10-31 22:37  菩提树下的杨过  阅读(109)  评论(0编辑  收藏  举报