LeetCode 第114题:二叉树展开为链表

108 阅读10分钟

LeetCode 第114题:二叉树展开为链表

题目描述

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

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

难度

中等

题目链接

点击在LeetCode中查看题目

示例

示例 1:

示例1

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

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [0]
输出:[0]

提示

  • 树中结点数在范围 [0, 2000]
  • -100 <= Node.val <= 100

解题思路

方法一:前序遍历 + 重建

最直观的方法是先对二叉树进行前序遍历,将所有节点保存在一个列表中,然后重建二叉树,使每个节点的左子节点为null,右子节点指向下一个节点。

关键点:

  • 使用前序遍历获取节点顺序
  • 重建二叉树,将每个节点的左子节点设为null,右子节点指向下一个节点

具体步骤:

  1. 如果根节点为空,直接返回
  2. 使用前序遍历将所有节点保存在列表中
  3. 遍历节点列表,将每个节点的左子节点设为null,右子节点指向下一个节点
  4. 最后一个节点的右子节点设为null

时间复杂度:O(n),其中n是树中节点的数量,需要遍历所有节点两次 空间复杂度:O(n),需要额外的列表存储所有节点

方法二:原地展开(迭代)

我们可以不使用额外空间,直接在原二叉树上进行操作。基本思路是对于当前节点,将其左子树插入到右子树的位置,然后将原来的右子树接到左子树的最右节点后面。

关键点:

  • 对于当前节点,找到其左子树的最右节点
  • 将当前节点的右子树接到左子树的最右节点后面
  • 将当前节点的左子树移到右子树的位置
  • 将当前节点的左子节点设为null
  • 移动到下一个节点继续操作

具体步骤:

  1. 如果根节点为空,直接返回
  2. 从根节点开始,对于当前节点: a. 如果当前节点的左子节点不为空: i. 找到左子树的最右节点 ii. 将当前节点的右子树接到左子树的最右节点的右子节点 iii. 将当前节点的左子树移到右子树的位置 iv. 将当前节点的左子节点设为null b. 移动到当前节点的右子节点继续操作

时间复杂度:O(n),每个节点最多被访问两次 空间复杂度:O(1),只使用常数额外空间

方法三:后序遍历(递归)

我们可以使用后序遍历的方式,从右到左、从下到上地处理节点,将已经展开的右子树和左子树连接起来。

关键点:

  • 后序遍历的顺序是左子树、右子树、根节点
  • 对于每个节点,先将右子树展开,再将左子树展开
  • 然后将展开后的左子树插入到根节点和展开后的右子树之间

具体步骤:

  1. 如果根节点为空,直接返回
  2. 递归展开右子树
  3. 递归展开左子树
  4. 如果左子树不为空: a. 找到展开后左子树的最右节点 b. 将展开后的右子树接到左子树的最右节点的右子节点 c. 将展开后的左子树移到右子树的位置 d. 将根节点的左子节点设为null

时间复杂度:O(n),每个节点最多被访问两次 空间复杂度:O(n),递归调用栈的深度

图解思路

方法一:前序遍历 + 重建过程

步骤操作结果
初始状态原始二叉树初始状态
前序遍历获取节点顺序[1, 2, 3, 4, 5, 6]
重建将节点按前序顺序连接1 -> 2 -> 3 -> 4 -> 5 -> 6

方法二:原地展开过程分析表

以示例1为例:

当前节点操作树的状态
1找到左子树(2,3,4)的最右节点4,将右子树(5,6)接到4的右子节点,将左子树移到右子树位置,左子节点设为null1 -> 2 -> 3 -> 4 -> 5 -> 6
2找到左子树(3)的最右节点3,将右子树(4,5,6)接到3的右子节点,将左子树移到右子树位置,左子节点设为null1 -> 2 -> 3 -> 4 -> 5 -> 6
3左子节点为空,移动到右子节点不变
4左子节点为空,移动到右子节点不变
5左子节点为空,移动到右子节点不变
6左子节点为空,右子节点也为空,结束不变

代码实现

C# 实现

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     public int val;
 *     public TreeNode left;
 *     public TreeNode right;
 *     public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
public class Solution {
    // 方法一:前序遍历 + 重建
    public void Flatten(TreeNode root) {
        // 如果根节点为空,直接返回
        if (root == null) {
            return;
        }
    
        // 使用前序遍历获取节点顺序
        List<TreeNode> nodes = new List<TreeNode>();
        PreorderTraversal(root, nodes);
    
        // 重建二叉树
        for (int i = 0; i < nodes.Count - 1; i++) {
            nodes[i].left = null;
            nodes[i].right = nodes[i + 1];
        }
    
        // 设置最后一个节点
        nodes[nodes.Count - 1].left = null;
        nodes[nodes.Count - 1].right = null;
    }
  
    private void PreorderTraversal(TreeNode root, List<TreeNode> nodes) {
        if (root == null) {
            return;
        }
    
        nodes.Add(root);
        PreorderTraversal(root.left, nodes);
        PreorderTraversal(root.right, nodes);
    }
  
    // 方法二:原地展开(迭代)
    public void FlattenIterative(TreeNode root) {
        // 如果根节点为空,直接返回
        if (root == null) {
            return;
        }
    
        TreeNode current = root;
    
        while (current != null) {
            // 如果当前节点有左子树
            if (current.left != null) {
                // 找到左子树的最右节点
                TreeNode rightmost = current.left;
                while (rightmost.right != null) {
                    rightmost = rightmost.right;
                }
            
                // 将当前节点的右子树接到左子树的最右节点后面
                rightmost.right = current.right;
            
                // 将左子树移到右子树的位置
                current.right = current.left;
            
                // 将左子节点设为null
                current.left = null;
            }
        
            // 移动到下一个节点
            current = current.right;
        }
    }
  
    // 方法三:后序遍历(递归)
    public void FlattenPostorder(TreeNode root) {
        // 如果根节点为空,直接返回
        if (root == null) {
            return;
        }
    
        // 递归展开右子树
        FlattenPostorder(root.right);
    
        // 递归展开左子树
        FlattenPostorder(root.left);
    
        // 如果左子树不为空
        if (root.left != null) {
            // 找到展开后左子树的最右节点
            TreeNode rightmost = root.left;
            while (rightmost.right != null) {
                rightmost = rightmost.right;
            }
        
            // 将展开后的右子树接到左子树的最右节点后面
            rightmost.right = root.right;
        
            // 将展开后的左子树移到右子树的位置
            root.right = root.left;
        
            // 将左子节点设为null
            root.left = null;
        }
    }
}

Python 实现

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    # 方法一:前序遍历 + 重建
    def flatten(self, root: Optional[TreeNode]) -> None:
        """
        Do not return anything, modify root in-place instead.
        """
        # 如果根节点为空,直接返回
        if not root:
            return
    
        # 使用前序遍历获取节点顺序
        nodes = []
    
        def preorder_traversal(node):
            if not node:
                return
        
            nodes.append(node)
            preorder_traversal(node.left)
            preorder_traversal(node.right)
    
        preorder_traversal(root)
    
        # 重建二叉树
        for i in range(len(nodes) - 1):
            nodes[i].left = None
            nodes[i].right = nodes[i + 1]
    
        # 设置最后一个节点
        nodes[-1].left = None
        nodes[-1].right = None
  
    # 方法二:原地展开(迭代)
    def flattenIterative(self, root: Optional[TreeNode]) -> None:
        # 如果根节点为空,直接返回
        if not root:
            return
    
        current = root
    
        while current:
            # 如果当前节点有左子树
            if current.left:
                # 找到左子树的最右节点
                rightmost = current.left
                while rightmost.right:
                    rightmost = rightmost.right
            
                # 将当前节点的右子树接到左子树的最右节点后面
                rightmost.right = current.right
            
                # 将左子树移到右子树的位置
                current.right = current.left
            
                # 将左子节点设为None
                current.left = None
        
            # 移动到下一个节点
            current = current.right
  
    # 方法三:后序遍历(递归)
    def flattenPostorder(self, root: Optional[TreeNode]) -> None:
        # 如果根节点为空,直接返回
        if not root:
            return
    
        # 递归展开右子树
        self.flattenPostorder(root.right)
    
        # 递归展开左子树
        self.flattenPostorder(root.left)
    
        # 如果左子树不为空
        if root.left:
            # 找到展开后左子树的最右节点
            rightmost = root.left
            while rightmost.right:
                rightmost = rightmost.right
        
            # 将展开后的右子树接到左子树的最右节点后面
            rightmost.right = root.right
        
            # 将展开后的左子树移到右子树的位置
            root.right = root.left
        
            # 将左子节点设为None
            root.left = None

C++ 实现

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    // 方法一:前序遍历 + 重建
    void flatten(TreeNode* root) {
        // 如果根节点为空,直接返回
        if (root == nullptr) {
            return;
        }
    
        // 使用前序遍历获取节点顺序
        vector<TreeNode*> nodes;
        preorderTraversal(root, nodes);
    
        // 重建二叉树
        for (int i = 0; i < nodes.size() - 1; i++) {
            nodes[i]->left = nullptr;
            nodes[i]->right = nodes[i + 1];
        }
    
        // 设置最后一个节点
        nodes.back()->left = nullptr;
        nodes.back()->right = nullptr;
    }
  
private:
    void preorderTraversal(TreeNode* root, vector<TreeNode*>& nodes) {
        if (root == nullptr) {
            return;
        }
    
        nodes.push_back(root);
        preorderTraversal(root->left, nodes);
        preorderTraversal(root->right, nodes);
    }
  
public:
    // 方法二:原地展开(迭代)
    void flattenIterative(TreeNode* root) {
        // 如果根节点为空,直接返回
        if (root == nullptr) {
            return;
        }
    
        TreeNode* current = root;
    
        while (current != nullptr) {
            // 如果当前节点有左子树
            if (current->left != nullptr) {
                // 找到左子树的最右节点
                TreeNode* rightmost = current->left;
                while (rightmost->right != nullptr) {
                    rightmost = rightmost->right;
                }
            
                // 将当前节点的右子树接到左子树的最右节点后面
                rightmost->right = current->right;
            
                // 将左子树移到右子树的位置
                current->right = current->left;
            
                // 将左子节点设为nullptr
                current->left = nullptr;
            }
        
            // 移动到下一个节点
            current = current->right;
        }
    }
  
    // 方法三:后序遍历(递归)
    void flattenPostorder(TreeNode* root) {
        // 如果根节点为空,直接返回
        if (root == nullptr) {
            return;
        }
    
        // 递归展开右子树
        flattenPostorder(root->right);
    
        // 递归展开左子树
        flattenPostorder(root->left);
    
        // 如果左子树不为空
        if (root->left != nullptr) {
            // 找到展开后左子树的最右节点
            TreeNode* rightmost = root->left;
            while (rightmost->right != nullptr) {
                rightmost = rightmost->right;
            }
        
            // 将展开后的右子树接到左子树的最右节点后面
            rightmost->right = root->right;
        
            // 将展开后的左子树移到右子树的位置
            root->right = root->left;
        
            // 将左子节点设为nullptr
            root->left = nullptr;
        }
    }
};

执行结果

C# 实现

  • 执行用时:88 ms
  • 内存消耗:38.2 MB

Python 实现

  • 执行用时:36 ms
  • 内存消耗:16.4 MB

C++ 实现

  • 执行用时:4 ms
  • 内存消耗:12.6 MB

性能对比

语言执行用时内存消耗特点
C#88 ms38.2 MB代码结构清晰,但性能较慢
Python36 ms16.4 MB代码简洁,性能适中
C++4 ms12.6 MB执行速度最快,内存占用最少

代码亮点

  1. 🎯 原地展开方法避免了额外空间的使用,直接在原二叉树上操作
  2. 💡 后序遍历方法利用了递归的特性,从下到上地处理节点,思路巧妙
  3. 🔍 正确处理了各种边界情况,如空树和只有一个节点的树
  4. 🎨 三种不同的实现方法各有优势,适用于不同的场景

常见错误分析

  1. 🚫 忘记处理左子树为空的情况,导致无限循环
  2. 🚫 在重建过程中没有正确设置最后一个节点的右子节点为null
  3. 🚫 在查找最右节点时没有正确遍历到最后一个节点
  4. 🚫 没有正确理解前序遍历的顺序,导致展开后的链表顺序错误

解法对比

解法时间复杂度空间复杂度优点缺点
前序遍历 + 重建O(n)O(n)实现简单,思路清晰需要额外空间存储节点
原地展开(迭代)O(n)O(1)不需要额外空间,效率高实现稍复杂
后序遍历(递归)O(n)O(n)代码简洁,思路巧妙递归调用栈可能导致栈溢出

相关题目