LEETCODE 114Medium
二叉树展开为链表
后序思维:左右子树各自先展开成链,再把左链整体插到根和右链之间——大问题只做一次“接线”。
问题拆解
把二叉树原地展开成一条只用 right 指针串起来的“链表”,节点顺序必须等于先序遍历。
最直白的做法是先做一遍先序遍历把节点存进列表,再依次重接指针——正确,但 O(n) 额外空间,而且“原地”的意味就没了。想不开辅助数组,就得在遍历的同时完成重接。
递归的分解方式很自然:假设左、右子树已经各自展开成链(这正是递归能提供的),根节点要做的只是接线——先序顺序是“根 → 左 → 右”,所以把展开后的左链搬到 root.right,原来的右链接到左链的末端,最后记得把 root.left 置空。难点只剩一个:找左链的末端,即一路向 right 走到底的那个节点。
后序处理的价值:等左右子树都变成“已展开的链”之后再动手,根节点面对的就不再是一棵树,而是两条头尾分明的链——接线成了纯粹的指针操作。
递归后序展开再接线
public void flatten(TreeNode root) {
if (root == null) {
return;
}
flatten(root.left);
flatten(root.right); // 左右子树先各自成链
TreeNode left = root.left;
if (left != null) {
TreeNode tail = left;
while (tail.right != null) {
tail = tail.right; // 找左链末端
}
tail.right = root.right; // 原右链接到左链末尾
root.right = left;
root.left = null; // 别忘了断开 left
}
}
def flatten(root: Optional[TreeNode]) -> None:
if not root:
return
flatten(root.left)
flatten(root.right) # 左右子树先各自成链
if root.left:
tail = root.left
while tail.right:
tail = tail.right # 找左链末端
tail.right = root.right # 原右链接到左链末尾
root.right = root.left
root.left = None # 别忘了断开 left
func flatten(root *TreeNode) {
if root == nil {
return
}
flatten(root.Left)
flatten(root.Right) // 左右子树先各自成链
if root.Left != nil {
tail := root.Left
for tail.Right != nil {
tail = tail.Right // 找左链末端
}
tail.Right = root.Right // 原右链接到左链末尾
root.Right = root.Left
root.Left = nil // 别忘了断开 left
}
}
pub fn flatten(root: &mut Option<Rc<RefCell<TreeNode>>>) {
if let Some(node) = root {
// 先取下左右子树,避免与后续 borrow_mut 冲突
let mut left = node.borrow_mut().left.take();
let mut right = node.borrow_mut().right.take();
Self::flatten(&mut left);
Self::flatten(&mut right); // 左右子树先各自成链
if left.is_some() {
// 找左链末端
let mut tail = Rc::clone(left.as_ref().unwrap());
loop {
let next = tail.borrow().right.clone();
match next {
Some(n) => tail = n,
None => break,
}
}
tail.borrow_mut().right = right; // 原右链接到左链末尾
node.borrow_mut().right = left;
} else {
node.borrow_mut().right = right;
}
}
}
最容易忘的一步是 root.left = null:指针都接对了,但左指针没断开,遍历结果看似正确,判题时按“left 必须全为空”检查就会挂掉。Rust 版还有一处值得注意:先用 take() 把左右子树从节点上摘下来再递归,这样后面 borrow_mut() 接线时就不会和活着的借用冲突;找末端时也不能一直握着 borrow() 往下走,要先 clone 出下一个 Rc 再释放当前借用,否则借用生命周期叠在一起直接编不过。
顺带一提,这题还有个精巧的 O(1) 空间迭代解(对每个有左子树的节点,找到左子树最右节点做“前驱”,把右链挂过去),思想同 Morris 遍历;递归版胜在思路直白,面试先写它不亏。
复杂度
| 指标 | 复杂度 | 原因 |
|---|---|---|
| 时间 | O(n) |
每个节点被递归访问一次;找末端沿的都是不重复的 right 边,总步数 O(n) |
| 空间 | O(h) |
递归栈深度为树高,最坏退化成 O(n) |
可以迁移的模式
- 改树形结构的题优先考虑后序:子树先处理完,根节点面对的是结构已知的成品;
- “把一段插入另一段”类指针操作,先找清楚四个关键点:插入段的头尾、断口的前后;
- Rust 树题的通用手法:
take()先摘子树、Rc::clone传递所有权、缩短每次borrow的存活范围。
递归定义即接口契约——相信“子调用返回时子树已经是链”,根节点的逻辑就只剩三行指针。