二叉树展开为链表[树处理的核心--如何处理任意一颗子树]

178 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,二叉树展开为链表[树处理的核心--如何处理任意一颗子树] - 掘金 (juejin.cn))

前言

对于树操作而言,遍历是基础。对于各种处理树问题,如果能懂如何处理好任意一棵子树,那么这棵树就被处理好了。通过二叉树展开为链表来练习所有处理树问题的核心:如何处理好任意一颗子树(只有root,只有左孩子,只有右孩子,两孩子都有)

一、二叉树展开为链表

image.png

二、处理树

1、暴力解决

把树节点按照前序遍历的前驱后继关系存入队列中,然后依次修改每个节点的left/right指针即可。

// 二叉树展开为链表。
public class Flatten {
    /*
    target:将二叉树展开为单向链表,即left=null,right指向后继。
    后继的定义为前序遍历的后继。
    直观方法:前序遍历把每个节点串起来。
     */
    public void flatten(TreeNode root) {
        preOrder(root);
        // 收集到节点队列,顺序改变其left,right指针。
        TreeNode head = que.poll();
        if (head != null) head.left = null;
        while (!que.isEmpty()) {
            TreeNode cur = que.poll();
            head.right = cur;
            cur.left = null;
            head = cur;
        }
    }

    Queue<TreeNode> que = new ArrayDeque<>();

    private void preOrder(TreeNode root) {
        if (root == null) return;
        que.offer(root);
        preOrder(root.left);
        preOrder(root.right);
    }

    // Definition for a binary tree node.
    public class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode() {
        }

        TreeNode(int val) {
            this.val = val;
        }

        TreeNode(int val, TreeNode left, TreeNode right) {
            this.val = val;
            this.left = left;
            this.right = right;
        }
    }

}

2、处理好任意一颗子树

进阶:O(1)的空间复杂度即原地算法,原地修改left/right,原地展开。

target:将二叉树展开为单向链表,即left=null,right指向后继。

后继的定义为前序遍历的后继。

O(1)空间复杂度:必须得搞清楚每一颗子树是如何处理left/right指针。

  1. 如果一开始就把right赋值了,那么就存在断链问题。

  2. 是否可以把right记录下来。接到左孩子的最右节点处,以此来满足前序遍历的前驱后继关系。

  3. 那么当前子树有四种情况,分别处理。四中情况:无孩子(叶子节点);有一个左孩子;有一个右孩子;有两个孩子。

// 进阶:O(1)的空间复杂度即原地算法,原地修改left/right,原地展开。
class Flatten2 {
    /*
    target:将二叉树展开为单向链表,即left=null,right指向后继。
    后继的定义为前序遍历的后继。
    O(1)空间复杂度:必须得搞清楚每一颗子树是如何处理left/right指针。
    如果一开始就把right赋值了,那么就存在断链问题。
    是否可以把right记录下来。接到左孩子的最右节点处,以此来满足前序遍历的前驱后继关系。
    那么当前子树有四种情况,分别处理。四中情况:无孩子(叶子节点);有一个左孩子;有一个右孩子;有两个孩子。
     */
    public void flatten(TreeNode root) {
        flattenTree(root);
    }

    private TreeNode flattenTree(TreeNode root) {
        // 递归到null节点上,底下再无子树,结束递归。
        if (root == null) return null;
        // 为了防止断链,先把right保存到当前栈帧里面。
        TreeNode right = root.right;
        // 修改left/right指针。
        root.right = root.left;
        root.left = null;
        // 往深处进行前序遍历,先左后右,但由于特殊情况,以右再接一个右。
        TreeNode left = flattenTree(root.right);
        // 四中情况:无孩子(叶子节点);有一个左孩子;有一个右孩子;有两个孩子。
        // 两个null,此栈帧里的root为叶子节点,它就是父节点左子树的最右节点。
        if (left == null && right == null) return root;
        // 左子树的最右节点不为null,右子树为null,直接返回左子树最右节点即可。
        if (left != null && right == null) return left;
        // 当前root左右孩子都有,则将记录的right接到左孩子的最右节点。最后进行递归修改left/right指针。
        if (left != null && right != null) {
            left.right = right;
        }
        else root.right = right;//没有左孩子,只有右孩子,那么就没有什么左子树的最右节点,那个节点就是自己root,把right接回来继续递归修改。
        // 继续递归修改,并返回右子树的最右节点,当作root这个子树的最右节点。
        return flattenTree(right);
    }

    // Definition for a binary tree node.
    public class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode() {
        }

        TreeNode(int val) {
            this.val = val;
        }

        TreeNode(int val, TreeNode left, TreeNode right) {
            this.val = val;
            this.left = left;
            this.right = right;
        }
    }

}

总结

  1. 所有处理树的核心都是处理好任意一颗子树即可,通过处理子树的分析,递归体也就出来了,问题迎刃而解。

参考文献

[1] LeetCode 二叉树展开为链表