二叉树题目:二叉树展开为链表

题目

标题和出处

标题:二叉树展开为链表

出处:114. 二叉树展开为链表

难度

3 级

题目描述

要求

给你二叉树的根结点 root \texttt{root} root,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode \texttt{TreeNode} TreeNode 类,其中右子结点指针指向链表中的下一个结点,左子结点指针总是 null \texttt{null} null
  • 展开后的单链表应该与二叉树前序遍历顺序相同。

示例

示例 1:

示例 1

输入: root   =   [1,2,5,3,4,null,6] \texttt{root = [1,2,5,3,4,null,6]} root = [1,2,5,3,4,null,6]
输出: [1,null,2,null,3,null,4,null,5,null,6] \texttt{[1,null,2,null,3,null,4,null,5,null,6]} [1,null,2,null,3,null,4,null,5,null,6]

示例 2:

输入: root   =   [] \texttt{root = []} root = []
输出: [] \texttt{[]} []

示例 3:

输入: root   =   [0] \texttt{root = [0]} root = [0]
输出: [0] \texttt{[0]} [0]

数据范围

  • 树中结点数目在范围 [0,   2000] \texttt{[0, 2000]} [0, 2000]
  • -100 ≤ Node.val ≤ 100 \texttt{-100} \le \texttt{Node.val} \le \texttt{100} -100Node.val100

进阶

你可以使用 O(1) \texttt{O(1)} O(1) 额外空间展开这棵树吗?

解法一

思路和算法

这道题要求按照二叉树前序遍历的顺序将二叉树展开为单链表。最直观的解法是对二叉树前序遍历并记录前序遍历的结点顺序,然后更改结点之间的指针指向。对于每个结点,将其左子结点指针设为 null \text{null} null,将其右子结点指针设为前序遍历顺序的后一个结点,其中前序遍历顺序的最后一个结点的右子结点指针也设为 null \text{null} null

代码

下面的代码为递归实现二叉树前序遍历的做法。

class Solution {
    List<TreeNode> traversal = new ArrayList<TreeNode>();

    public void flatten(TreeNode root) {
        preorder(root);
        int size = traversal.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = traversal.get(i);
            node.left = null;
            node.right = i == size - 1 ? null : traversal.get(i + 1);
        }
    }

    public void preorder(TreeNode node) {
        if (node == null) {
            return;
        }
        traversal.add(node);
        preorder(node.left);
        preorder(node.right);
    }
}

下面的代码为迭代实现二叉树前序遍历的做法。

class Solution {
    public void flatten(TreeNode root) {
        List<TreeNode> traversal = new ArrayList<TreeNode>();
        Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
        TreeNode node = root;
        while (!stack.isEmpty() || node != null) {
            while (node != null) {
                traversal.add(node);
                stack.push(node);
                node = node.left;
            }
            node = stack.pop().right;
        }
        int size = traversal.size();
        for (int i = 0; i < size; i++) {
            node = traversal.get(i);
            node.left = null;
            node.right = i == size - 1 ? null : traversal.get(i + 1);
        }
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。前序遍历需要访问每个结点一次,展开为链表也需要访问每个结点一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。前序遍历的递归实现和迭代实现都需要栈空间,栈空间取决于二叉树的高度,最坏情况下二叉树的高度是 O ( n ) O(n) O(n)

解法二

思路和算法

解法一需要在前序遍历之后将二叉树展开为链表,因为展开为链表的过程会改变结点之间的指针关系,破坏二叉树的结构,导致丢失子结点的信息。

之所以会丢失子结点的信息,是因为在对左子树遍历时,没有存储右子结点的信息,在遍历完左子树之后才获得右子结点的信息。为了不丢失子结点的信息,可以修改前序遍历的迭代实现,在遍历左子树之前就获得右子结点的信息并存入栈内,此时可以在前序遍历的同时将二叉树展开为链表。

修改后的前序遍历的做法是,每次将当前访问的结点出栈,如果该结点的子结点不为空,则依次将右子结点和左子结点入栈。注意入栈顺序不可颠倒,因为左子结点先被访问因此需要先出栈。

展开为单链表的做法是,维护上一个访问的结点 prev \textit{prev} prev 和当前访问的结点 curr \textit{curr} curr,初始时 prev = null \textit{prev} = \text{null} prev=null。对于每个结点,当 prev ≠ null \textit{prev} \ne \text{null} prev=null 时,将 prev \textit{prev} prev 的左子结点指针设为 null \text{null} null,将 prev \textit{prev} prev 的右子结点指针设为 curr \textit{curr} curr。访问当前结点 curr \textit{curr} curr 之后,将 prev \textit{prev} prev 的值设为 curr \textit{curr} curr,继续访问下一个结点,直到遍历结束。

代码

class Solution {
    public void flatten(TreeNode root) {
        if (root == null) {
            return;
        }
        Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
        stack.push(root);
        TreeNode prev = null;
        while (!stack.isEmpty()) {
            TreeNode curr = stack.pop();
            if (prev != null) {
                prev.left = null;
                prev.right = curr;
            }
            if (curr.right != null) {
                stack.push(curr.right);
            }
            if (curr.left != null) {
                stack.push(curr.left);
            }
            prev = curr;
        }
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。每个结点都被访问一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。空间复杂度主要是栈空间,栈内元素个数不超过 n n n

解法三

思路和算法

使用常数空间实现二叉树展开为链表,则不能使用栈或者其他数据结构存储结点,只能改变结点之间的指针关系。

在遍历二叉树的过程中改变结点之间的指针关系,将二叉树展开为链表。对于每个结点,考虑子结点的情况。

  • 如果没有子结点,则该结点是叶结点,遍历结束。

  • 如果只有左子结点,则将左子树变成右子树,然后移动到右子结点继续展开剩下的结点。

  • 如果只有右子结点,则移动到右子结点继续展开剩下的结点。

  • 如果左子结点和右子结点都存在,则根据前序遍历顺序,遍历完左子树之后访问右子结点,因此需要找到左子树中最后被访问的结点,即当前结点的前驱结点,前驱结点为当前结点的左子树中的最右边的结点。找到当前结点的前驱结点之后,操作如下。

    1. 将当前结点的右子树移动到前驱结点的右子树的位置。

    2. 将当前结点的左子树移动到当前结点的右子树的位置。

    3. 将当前结点的左子结点设为空。

左子结点和右子结点都存在的情况的展开操作也适用于只有左子结点的情况,因此对于每个结点的展开操作如下。

  1. 如果当前结点的左子结点不为空,则执行上述展开操作。如果当前结点的左子结点为空,则不执行展开操作。

  2. 移动到当前结点的右子结点。

重复上述操作,直到当前结点变为空,此时二叉树展开为链表的操作结束。

考虑如下二叉树,共有 7 7 7 个结点,其前序遍历顺序是 [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] [1,2,3,4,5,6,7] [1,2,3,4,5,6,7]

图 1

从根结点 1 1 1 开始遍历。结点 1 1 1 的左子结点不为空,因此找到结点 1 1 1 的前驱结点 4 4 4,将以结点 5 5 5 为根结点的右子树移动到结点 4 4 4 的右子树的位置,然后将结点 1 1 1 的左子树(以结点 2 2 2 为根结点)移动到右子树的位置。

图 2

结点 2 2 2 的左子结点不为空,因此找到结点 2 2 2 的前驱结点 3 3 3,将以结点 4 4 4 为根结点的右子树移动到结点 3 3 3 的右子树的位置,然后将结点 2 2 2 的左子树(以结点 3 3 3 为根结点)移动到右子树的位置。

在这里插入图片描述

结点 3 3 3 和结点 4 4 4 的左子结点都为空,因此不执行展开操作。

结点 5 5 5 的左子结点不为空,因此找到结点 5 5 5 的前驱结点 6 6 6,将以结点 7 7 7 为根结点的右子树移动到结点 6 6 6 的右子树的位置,然后将结点 5 5 5 的左子树(以结点 6 6 6 为根结点)移动到右子树的位置。

图 4
结点 6 6 6 和结点 7 7 7 的左子结点都为空,因此不执行展开操作。

遍历结束,此时得到二叉树展开后的链表。

代码

class Solution {
    public void flatten(TreeNode root) {
        TreeNode node = root;
        while (node != null) {
            if (node.left != null) {
                TreeNode predecessor = node.left;
                while (predecessor.right != null) {
                    predecessor = predecessor.right;
                }
                predecessor.right = node.right;
                node.right = node.left;
                node.left = null;
            }
            node = node.right;
        }
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。遍历和展开过程中,每个结点都被访问一次,寻找前驱结点的过程中,每个结点最多被访问一次,因此每个结点最多被访问两次。

  • 空间复杂度: O ( 1 ) O(1) O(1)

后记

读者也许已经发现,解法三和莫里斯遍历非常相似。和莫里斯遍历相比,这道题不需要将前驱结点的右指针指向当前结点,而是将当前结点的右子树移动到前驱结点的右子树的位置,因此解法三可以看成莫里斯遍历的简化版。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伟大的车尔尼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值