展开二叉树

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

  • 展开后的单链表继续使用 TreeNode,其中 right 子指针指向链表中的下一个节点,left 子指针始终为 nullptr
  • 展开后的单链表顺序应该和二叉树的先序遍历顺序相同。

示例 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

进阶: 可以使用原地算法,即只使用 O(1) 额外空间展开这棵树吗?

解法一(递归反向先序遍历):

/**
 * 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 {
private:
    TreeNode* previous = nullptr;

public:
    void flatten(TreeNode* root) {
        if (root == nullptr)
        {
            return;
        }

        flatten(root->right);
        flatten(root->left);

        root->right = previous;
        root->left = nullptr;
        previous = root;
    }
};

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

/**
 * 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) {
        TreeNode* current = root;

        while (current != nullptr)
        {
            if (current->left != nullptr)
            {
                TreeNode* predecessor = current->left;

                while (predecessor->right != nullptr)
                {
                    predecessor = predecessor->right;
                }

                predecessor->right = current->right;
                current->right = current->left;
                current->left = nullptr;
            }

            current = current->right;
        }
    }
};

核心思想

题目要求展开后的链表顺序和先序遍历顺序相同。

先序遍历顺序是:

根节点 -> 左子树 -> 右子树

所以对于当前节点 root,展开后的顺序应该是:

root -> flatten(root->left) -> flatten(root->right)

关键在于,展开过程中不能丢失原来的右子树。

可以先把当前节点的左子树展开,再把原来的右子树接到左子树展开结果的末尾。

这题最关键的观察是:

当前节点的左子树展开后,左子树中先序遍历的最后一个节点,就是整个左子树链表的尾节点。

因此可以执行以下操作:

  1. 找到当前节点左子树中最右侧的节点,也就是左子树先序展开后的尾节点。
  2. 把当前节点原来的右子树接到这个尾节点后面。
  3. 把当前节点的左子树整体移到右边。
  4. 把当前节点的左指针置为 nullptr

这样就完成了当前节点的局部调整,并且没有创建新节点。

解法一:递归反向先序遍历

如果按照正常先序遍历访问节点,需要先处理当前节点,再处理左子树和右子树。

但当前节点要连接到“先序遍历中的下一个节点”,这个下一个节点只有在左子树和右子树处理完成后才容易确定。

因此可以反过来处理先序遍历:

右子树 -> 左子树 -> 根节点

这相当于先序遍历序列的逆序。

previous 保存当前节点在展开链表中的后继节点。

递归处理顺序是:

flatten(root->right);
flatten(root->left);

处理完成后,previous 正好指向当前节点在先序序列中的下一个节点。

于是把当前节点接到 previous 前面:

root->right = previous;
root->left = nullptr;
previous = root;

最终所有节点都会按照先序顺序通过 right 指针连接起来。

为什么递归时要先处理右子树

假设先序遍历顺序是:

根 -> 左 -> 右

那么逆序就是:

右 -> 左 -> 根

例如这棵树:

    1
   / \
  2   5
 / \
3   4
     \
      6

先序遍历顺序是:

1,2,3,4,6,5

逆序处理顺序是:

5,6,4,3,2,1

处理 5 时,它的后继为空。

处理 6 时,把 6->right 指向 5

处理 4 时,把 4->right 指向 6

依次处理后,previous 最终会形成:

1 -> 2 -> 3 -> 4 -> 6 -> 5

所以“先右后左”的递归顺序,是为了从后往前建立先序链表。

解法二:迭代原地展开

迭代解法不使用递归,也不使用额外容器。

current 从根节点开始,沿着最终链表的 right 指针向后移动。

如果当前节点没有左子树,那么它已经符合链表结构,直接移动到右孩子。

如果当前节点有左子树,就需要把左子树插入到当前节点和原右子树之间。

假设当前结构是:

current
   /   \
 left  right

处理后要变成:

current -> left 子树展开结果 -> right 子树

左子树先序遍历的最后一个节点,是左子树中最右侧的节点。

因此先找到它:

TreeNode* predecessor = current->left;

while (predecessor->right != nullptr)
{
    predecessor = predecessor->right;
}

然后执行三步指针调整:

predecessor->right = current->right;
current->right = current->left;
current->left = nullptr;

第一步保存原来的右子树。

第二步把左子树移到右侧,符合先序顺序。

第三步清空左指针,满足单链表要求。

完成后继续处理:

current = current->right;

为什么左子树最右节点是展开后的尾节点

对一棵子树进行先序遍历时,顺序是:

根节点 -> 左子树 -> 右子树

因此,先序遍历的最后一个节点一定位于这棵子树的最右侧路径上。

在当前局部结构中,左子树还没有被移动之前,它的最右侧节点就是左子树先序展开结果的尾节点。

把当前节点原来的右子树接到这个节点后面,就能得到:

当前节点 -> 左子树先序序列 -> 原右子树先序序列

这正好是当前子树的先序序列。

为什么不会丢失原来的右子树

如果直接执行:

current->right = current->left;

原来的右子树指针就会被覆盖,导致右子树丢失。

所以必须先把原来的右子树保存到左子树展开结果的尾部:

predecessor->right = current->right;

此时原右子树仍然可以从 predecessor 访问到。

之后再执行:

current->right = current->left;

就不会丢失任何节点。

边界情况

如果二叉树为空:

root = []

没有节点需要展开,直接返回。

如果二叉树只有一个节点,它没有左右孩子,展开后仍然是这个节点,左指针为 nullptr

如果当前节点只有左子树,左子树会被移动到右侧,原来的右指针为空。

如果当前节点只有右子树,没有左子树,不需要调整,直接继续处理右子树。

如果树退化成一条左链,每次都会把左孩子移动到右边,最后得到一条从上到下的右链。

如果树退化成一条右链,原结构已经符合先序展开结果,不需要修改。

正确性证明

我们证明:两个解法都能将二叉树原地展开为符合先序遍历顺序的单链表。

结论 1:递归解法处理节点时,previous 保存其先序后继

设当前节点为 root

递归先处理 root 的右子树,再处理左子树,最后处理 root

这正是先序序列:

root -> 左子树 -> 右子树

的逆序。

因此,当处理 root 时,先序序列中位于 root 后面的所有节点都已经通过 right 指针连接好,并且链表头节点就是 previous

所以 previous 正好是 root 在先序序列中的后继节点。

结论 2:递归解法连接出的顺序是先序顺序

处理当前节点时,算法执行:

root->right = previous;
root->left = nullptr;
previous = root;

这会把当前节点放到已经构造好的后继链表前面。

由于节点是按照先序序列的逆序被处理,所以每次把当前节点放到前面后,链表顺序就恢复为先序顺序。

因此最终链表节点顺序和原树的先序遍历顺序相同。

结论 3:迭代解法的局部调整不会改变当前子树的先序顺序

考虑当前节点 current

它原来的先序顺序是:

current -> 左子树 -> 右子树

如果存在左子树,算法找到左子树最右侧节点 predecessor,并执行:

predecessor->right = current->right;
current->right = current->left;
current->left = nullptr;

调整后,从 current 沿 right 指针访问到的顺序变为:

current -> 左子树 -> 原右子树

左子树和右子树内部的相对顺序没有改变。

所以当前子树的先序顺序保持不变。

结论 4:迭代解法不会丢失任何节点

在修改当前节点之前,算法先将原来的右子树连接到左子树的尾节点:

predecessor->right = current->right;

因此原右子树仍然可以从当前节点的新 right 链路访问到。

左子树则通过:

current->right = current->left;

移动到当前节点右侧。

最后只把已经转移的左指针置空,不删除任何节点。

因此每个原节点都会保留下来,不会丢失。

结论 5:最终所有节点的左指针都为空

递归解法在处理每个节点时都会执行:

root->left = nullptr;

迭代解法在把左子树移动到右侧后,也会执行:

current->left = nullptr;

每个节点都会被处理一次。

所以最终所有节点的左指针都为空。

得出结论

由结论 1 和结论 2 可知,递归解法能按照先序顺序连接所有节点。

由结论 3 可知,迭代解法每次局部调整都保持先序顺序。

由结论 4 可知,迭代调整不会丢失任何节点。

由结论 5 可知,最终所有节点都满足左指针为空的单链表要求。

因此两个解法都能正确完成二叉树展开。

举例理解

以:

root = [1,2,5,3,4,null,6]

为例,这棵树可以理解为:

    1
   / \
  2   5
 / \   \
3   4   6

原树的先序遍历顺序是:

1,2,3,4,5,6

使用迭代原地展开:

处理节点 1

节点 1 有左子树。

左子树最右侧节点是 4

先把原右子树 5 接到 4 后面:

4 -> 5

再把左子树移动到 1 的右侧:

1 -> 2 -> 3/4 -> 5 -> 6

并将 1->left 置空。

处理节点 2

节点 2 仍有左子树,左子树最右侧节点是 3

2 的原右子树 4 接到 3 后面,再把左子树移动到右侧:

2 -> 3 -> 4

处理后续节点

节点 34 没有左子树,直接向右移动。

最终得到:

1 -> 2 -> 3 -> 4 -> 5 -> 6

所有节点的左指针都为空。

复杂度分析

设二叉树节点数为 n,树的高度为 h

解法一

每个节点只会被递归访问一次。

  • 时间复杂度:O(n)
  • 空间复杂度:O(h)

空间复杂度来自递归调用栈。

解法二

每个节点都会沿着最终的 right 链路被处理一次。

寻找左子树最右节点时,树中的指针会被有限次访问,所有遍历操作的总复杂度为 O(n)

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

解法二不使用递归栈,也不创建数组或其他辅助节点,满足题目的进阶要求。