二叉树展开为链表详解
- 问题描述
- 示例
- 提示
- 题目理解
- 解题思路
- 迭代实现(Morris遍历变体)
- 代码解析
- 图解过程
- 复杂度分析
- 其他解法
- 1. 递归解法(使用先序遍历)
- 2. 递归后序遍历解法
- 总结
问题描述
给你二叉树的根结点 root,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用 TreeNode,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
- 展开后的单链表应该与二叉树先序遍历顺序相同
示例
示例 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
题目理解
这道题要求我们将一棵二叉树按照先序遍历的顺序展开成一个单链表。转换后:
- 所有节点的左子树必须为空
- 所有节点的右子树指向下一个节点
- 顺序必须与先序遍历(根-左-右)一致
解题思路
该题有多种解法,这里介绍本题给出的原地算法(Morris遍历的变体),该算法空间复杂度为O(1)。
迭代实现(Morris遍历变体)
基本思想:
- 对于当前节点,如果其左子树不为空:
- 找到左子树的最右节点(先序遍历的前驱节点)
- 将当前节点的右子树接到这个最右节点的右子树上
- 然后将当前节点的左子树移到右子树的位置
- 将左子树置为NULL
- 处理完当前节点后,继续处理下一个节点(cur->right)
/*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/
void flatten(struct TreeNode* root) {struct TreeNode* cur = root;while (cur != NULL) {if (cur->left != NULL) {// 找到当前节点左子树的最右节点(也就是cur节点的前驱节点)struct TreeNode* next = cur->left; // 保存左节点struct TreeNode* predecessor = next;while (predecessor->right != NULL) {predecessor = predecessor->right;}// 将当前节点的右子树接到前驱节点的右子树上predecessor->right = cur->right;// 将当前节点的左子树移到右子树位置cur->right = next;cur->left = NULL; // 左子树置为NULL}// 继续处理下一个节点cur = cur->right;}
}
代码解析
- 首先,从根节点开始,遍历每个节点。
- 对于当前节点
cur
:- 如果它有左子树,我们需要做一些处理
- 如果没有左子树,直接处理下一个节点
- 当有左子树时:
- 保存左子节点到
next
- 找到左子树中的最右节点
predecessor
(这将是先序遍历中当前节点的前驱) - 将当前节点的右子树挂到
predecessor
的右子树上 - 将左子树移到右子树的位置
- 将左子树清空(设置为NULL)
- 保存左子节点到
- 然后处理下一个节点
cur = cur->right
图解过程
以示例1为例,树的结构如下:
1/ \2 5/ \ \
3 4 6
-
起始状态:cur = 1
- 1有左子树,找到左子树2的最右节点4
- 将1的右子树5挂到4的右子树
- 将1的左子树2移到右子树位置,左子树置NULL
1\2/ \ 3 4\5\6
-
cur = 2
- 2有左子树,找到左子树3的最右节点3
- 将2的右子树4挂到3的右子树
- 将2的左子树3移到右子树位置,左子树置NULL
1\2\3\4\5\6
-
cur = 3
- 3没有左子树,直接处理下一个节点
-
cur = 4
- 4没有左子树,直接处理下一个节点
-
cur = 5
- 5没有左子树,直接处理下一个节点
-
cur = 6
- 6没有左子树,直接处理结束
最终结果:
1 -> 2 -> 3 -> 4 -> 5 -> 6
复杂度分析
- 时间复杂度:O(n),其中n是树中节点的个数。每个节点最多会被访问两次。
- 空间复杂度:O(1),只使用了常数额外空间。
其他解法
除了上述Morris遍历的变体外,还有几种常见解法:
1. 递归解法(使用先序遍历)
思路:先通过先序遍历得到所有节点的顺序,然后按顺序连接起来。
void preorder(struct TreeNode* root, struct TreeNode** list, int* size) {if (!root) return;list[(*size)++] = root;preorder(root->left, list, size);preorder(root->right, list, size);
}void flatten(struct TreeNode* root) {if (!root) return;// 先序遍历得到节点顺序struct TreeNode* list[2000];int size = 0;preorder(root, list, &size);// 连接成链表for (int i = 1; i < size; i++) {list[i-1]->right = list[i];list[i-1]->left = NULL;}list[size-1]->left = NULL;list[size-1]->right = NULL;
}
空间复杂度:O(n)
2. 递归后序遍历解法
思路:利用后序遍历(右-左-根)的特点,维护上一个处理的节点prev。
struct TreeNode* prev = NULL;void flatten(struct TreeNode* root) {if (!root) return;// 后序遍历:右-左-根flatten(root->right);flatten(root->left);// 处理当前节点root->right = prev;root->left = NULL;prev = root;
}
空间复杂度:O(h),h为树的高度,递归调用栈的开销。
总结
本题展示了如何将二叉树展开为链表,关键在于理解先序遍历和链表结构之间的转换关系。给出的原地算法(Morris遍历变体)提供了一种优雅的O(1)空间复杂度解法,通过重组树结构实现了链表的转换。