二叉树展开为链表 图解题解
从后往前接,一个指针就能把整棵树原地串成一条链。
前序遍历是「根、左、右」;如果从右、左、根倒着走,正好是前序的逆序。把这条逆序的路记成一根绳,每走到一个节点就让它的 right 指向绳子最近的那头,left 清空——相当于从尾部往前把绳子一节节接起来。最后整棵树就顺着 right 串成了一条直线,而你全程没有额外建数组,只用了一个「prev 指针」。
这道题到底在问什么
- 输入
- [1,2,5,3,4,null,6]
- 输出
- 1 → 2 → 3 → 9 → 4 → 5 → 6 → 7 → 8
最优解:为什么这么做
一句话答案:LeetCode 114 二叉树展开为链表的进阶解法是 O(1) 空间的原地指针法:cur 沿右指针逐个处理节点,遇到有左子树的,就沿左子树的右指针走到当前右链末端,把原右子树先接到这个末端节点后面,再把整棵左子树搬到右边、左指针置空。总时间 O(n)、额外空间 O(1),原地完成。
题目要求的链表长什么样
题目要求把二叉树原地改造成一条只用右指针串起来的单链表:每个节点的 left 都置空,right 指向它在先序遍历(根、左、右)中的下一个节点。题目同时提出两个要求:节点顺序必须严格等于先序,并且要在原来这棵树上直接改指针,不另外新建结构。
先存先序序列再重接,代价在哪里
最直接的做法是先做一遍先序遍历,把节点按顺序存进列表,再从头到尾把 right 逐个接上、left 置空。它正确且好写,但要 O(n) 的额外空间存整个序列;改成递归原地拼接可以省掉列表,递归栈仍要 O(h)。想做到真正的 O(1) 额外空间,就得在不记录任何全局顺序的前提下,靠局部的指针操作一段一段地把树拉直。
关键观察:原右子树该临时接到哪里
回到先序的定义,考察任意一个节点 cur:cur 之后紧跟的是它的整棵左子树,左子树全部走完才轮到原来的右子树。所以把左子树搬到右边之前,必须先给原右子树找一个不破坏顺序的落脚点。做法是沿左子树的右指针一路走到底,找到当前右链的末端节点,把原右子树接到它后面。注意这个末端节点不一定是左子树先序的最后一个节点——左子树里可能还有没展开的左分支——但挂在这里不会出错:先序先左后右,挂上去的整段仍然排在左子树现有全部节点之后;等 cur 往下走到那些更深的左分支时,同样的操作会把这段继续往下推,最终恰好落到完整先序的正确位置。这个「沿右链找接驳点」的手法与 Morris 遍历找前驱是同款技巧。
每一步搬运为什么不丢节点、拼出来恰好是先序
对每个有左子树的节点做三个动作,次序不能乱:先让右链末端节点接管原右子树——先把右子树托付出去,它才不会在下一步被覆盖丢失;再把整棵左子树搬到 right 上;最后把 left 置空。这一轮结束时保证的是三件事:原右子树完好挂在树里没有脱链、左子树整体挪到了 cur 的右侧、cur 本身满足「左空右连」的链表形态——完整的先序顺序并不靠这一轮一次做完,而是 cur 沿 right 前进后,对后面每个还有左子树的节点重复同样的操作,一段一段逐步成形。
两个高频错误:先搬左子树再想起原右子树,此时右指针已被覆盖、整棵右子树永久丢失;搬完忘记把 cur.left 置空,结果不满足「只有右孩子」的链表形态。
为什么总时间还是线性 O(n)
看起来「找最右节点」像嵌套循环,其实不然:每个节点作为某次寻找前驱的途经点,总共只会被走过常数次,所有寻找加起来的步数是线性的,时间 O(n)。空间上只用 cur、pre 两个指针,既无递归也无栈,额外空间 O(1)——这正是它比「先序列表重接」和递归拼接更进一步的地方。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3诀窍在于:对每个有左子树的节点,左子树这一整段在先序里就排在它后面、原右子树前面。所以把左子树最右的节点接上原右子树,再把左子树搬到右边、左指针清空——一段就拉直了。下面逐步看。
- 4先序 = 1 → 2 → 3 → 9 → 4 → 5 → 6 → 7 → 8先看清原始树:根是 1,有左子树(2…)和右子树(5…)。它的先序遍历是 1 → 2 → 3 → 9 → 4 → 5 → 6 → 7 → 8——这正是展开后链表的节点顺序。接下来分两步:先走一遍先序看清目标顺序,再逐段把树拉直。
- 5先序: 1展开的目标顺序,就是这棵树的先序遍历(根 → 左子树 → 右子树)。从根 1 出发,先访问根自己。
- 6先序: 1 → 2先序的规矩是「根、左、右」,所以走到节点 2。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 7先序: 1 → 2 → 3先序的规矩是「根、左、右」,所以走到节点 3。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 8先序: 1 → 2 → 3 → 9先序的规矩是「根、左、右」,所以走到节点 9。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 9先序: 1 → 2 → 3 → 9 → 4先序的规矩是「根、左、右」,所以走到节点 4。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 10先序: 1 → 2 → 3 → 9 → 4 → 5先序的规矩是「根、左、右」,所以走到节点 5。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 11先序: 1 → 2 → 3 → 9 → 4 → 5 → 6先序的规矩是「根、左、右」,所以走到节点 6。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 12先序: 1 → 2 → 3 → 9 → 4 → 5 → 6 → 7先序的规矩是「根、左、右」,所以走到节点 7。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 13先序: 1 → 2 → 3 → 9 → 4 → 5 → 6 → 7 → 8先序的规矩是「根、左、右」,所以走到节点 8。把它接到序列末尾——展开后的链表,节点顺序就得严格按这个先序排。
- 141.left=2 → 待右斜化;左子树先序末尾=4轮到节点 1,它还挂着左子树。先序里,左子树整段要排在 1 之后、原右子树之前。所以先找到左子树最右节点 4(蓝)——它将成为接驳点。
- 151.right=2, 1.left=∅, 4.right=5三步指针操作:① 把 1 原来的右子树接到 4.right;② 把左子树整段搬到 1.right;③ 1.left 置空。现在 1 只剩右孩子,左子树乖乖排到了它后面——这一段先序就被「拉直」了。
- 162.left=3 → 待右斜化;左子树先序末尾=3轮到节点 2,它还挂着左子树。先序里,左子树整段要排在 2 之后、原右子树之前。所以先找到左子树最右节点 3(蓝)——它将成为接驳点。
- 172.right=3, 2.left=∅, 3.right=4三步指针操作:① 把 2 原来的右子树接到 3.right;② 把左子树整段搬到 2.right;③ 2.left 置空。现在 2 只剩右孩子,左子树乖乖排到了它后面——这一段先序就被「拉直」了。
- 183.left=9 → 待右斜化;左子树先序末尾=9轮到节点 3,它还挂着左子树。先序里,左子树整段要排在 3 之后、原右子树之前。所以先找到左子树最右节点 9(蓝)——它将成为接驳点。
- 193.right=9, 3.left=∅, 9.right=4三步指针操作:① 把 3 原来的右子树接到 9.right;② 把左子树整段搬到 3.right;③ 3.left 置空。现在 3 只剩右孩子,左子树乖乖排到了它后面——这一段先序就被「拉直」了。
- 209.left=∅,已在右链上,cur → 4节点 9 没有左子树,它本来就只有右孩子,已经在右链上了,不用动。指针顺着右孩子继续往下,去处理 下一个节点 4。
- 214.left=∅,已在右链上,cur → 5节点 4 没有左子树,它本来就只有右孩子,已经在右链上了,不用动。指针顺着右孩子继续往下,去处理 下一个节点 5。
- 225.left=∅,已在右链上,cur → 6节点 5 没有左子树,它本来就只有右孩子,已经在右链上了,不用动。指针顺着右孩子继续往下,去处理 下一个节点 6。
- 236.left=7 → 待右斜化;左子树先序末尾=7轮到节点 6,它还挂着左子树。先序里,左子树整段要排在 6 之后、原右子树之前。所以先找到左子树最右节点 7(蓝)——它将成为接驳点。
- 246.right=7, 6.left=∅, 7.right=8三步指针操作:① 把 6 原来的右子树接到 7.right;② 把左子树整段搬到 6.right;③ 6.left 置空。现在 6 只剩右孩子,左子树乖乖排到了它后面——这一段先序就被「拉直」了。
- 257.left=∅,已在右链上,cur → 8节点 7 没有左子树,它本来就只有右孩子,已经在右链上了,不用动。指针顺着右孩子继续往下,去处理 下一个节点 8。
- 268.left=∅,已在右链上,cur → null节点 8 没有左子树,它本来就只有右孩子,已经在右链上了,不用动。指针顺着右孩子继续往下,去处理 ——到这已是链尾。
- 27右链 = 1 → 2 → 3 → 9 → 4 → 5 → 6 → 7 → 8全部拉直!现在每个节点都只有右孩子,从 1 一路沿右指针垂到 8,顺序 1 → 2 → 3 → 9 → 4 → 5 → 6 → 7 → 8 正好是先序遍历。这就是"二叉树展开为链表"。
⚠️ 容易写错的地方
✗ 错:先把 left 接到 right 再保存原右子树
✓ 对:先用 pre.right 接住原右子树,再搬左子树
顺序反了会丢失原右子树
✗ 错:忘记把 cur.left 置空
✓ 对:左子树搬走后必须 cur.left=null
不清空就不是「只有右孩子」的链表
✗ 错:展开顺序当成中序/层序
✓ 对:必须是先序(根→左→右)
题目明确要求先序顺序
完整代码(Java / Python / C++)
Java
class Solution {
public void flatten(TreeNode root) {
TreeNode cur = root;
while (cur != null) {
if (cur.left != null) {
TreeNode pre = cur.left; // 左子树
while (pre.right != null) // 找左子树最右(先序末尾)
pre = pre.right;
pre.right = cur.right; // 原右子树接到它后面
cur.right = cur.left; // 左子树整段搬到右边
cur.left = null; // 左指针清空
}
cur = cur.right; // 沿右脊前进
}
}
}Python
class Solution:
def flatten(self, root: TreeNode) -> None:
cur = root
while cur:
if cur.left:
pre = cur.left # 左子树
while pre.right: # 找左子树最右(先序末尾)
pre = pre.right
pre.right = cur.right # 原右子树接到它后面
cur.right = cur.left # 左子树整段搬到右边
cur.left = None # 左指针清空
cur = cur.right # 沿右脊前进C++
class Solution {
public:
void flatten(TreeNode* root) {
TreeNode* cur = root;
while (cur != nullptr) {
if (cur->left != nullptr) {
TreeNode* pre = cur->left; // 左子树
while (pre->right != nullptr) // 找左子树最右(先序末尾)
pre = pre->right;
pre->right = cur->right; // 原右子树接到它后面
cur->right = cur->left; // 左子树整段搬到右边
cur->left = nullptr; // 左指针清空
}
cur = cur->right; // 沿右脊前进
}
}
};复杂度
时间
O(n)
每节点最多被「沿右脊找最右」访问常数次,总体线性
空间
O(1)
只用 cur/pre 两个指针,原地改指针、无递归栈
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 二叉树展开为链表 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这道题为什么用「二叉树」,换最直接的暴力解会差在哪?+
二叉树抓住了本题的结构特征,把暴力解里重复的工作省掉;暴力解通常要多嵌套一层枚举,数据一大就超时。具体对比见上文「暴力解及其卡点」与「最优解逐步推演」两节。
时间复杂度为什么是 undefined?怎么推出来的?+
按上文复杂度小节的推导,时间复杂度为 undefined。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 二叉树展开为链表 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。