114. 二叉树展开为链表
class Solution {
TreeNode *Prev;
TreeNode *Head;
void traverse(TreeNode *TN, int Depth) {
if (!TN)
return;
if (Prev) {
Prev->right = new TreeNode(TN->val);
Prev = Prev->right;
} else {
Head = Prev = new TreeNode(TN->val);
}
traverse(TN->left, Depth + 1);
traverse(TN->right, Depth + 1);
if (Depth > 0)
delete TN;
}
public:
void flatten(TreeNode* root) {
if (!root)
return;
traverse(root, 0);
*root = *Head;
delete Head;
}
};
我这个解法完全是 hack 来的,有点崩溃。
这道二刷的时候要用前序、反前序和前驱思路再写一遍。
二刷
class Solution {
public:
void flatten(TreeNode* root) {
if (!root)
return;
// Process left tree.
flatten(root->left);
// When finished, move left tree onto right branch.
TreeNode *Right = root->right;
root->right = root->left;
// And connect to original branch.
TreeNode *Temp = root;
TreeNode Dummy(0, nullptr, root), *Prev = &Dummy;
while (Temp) {
Temp = Temp->right;
Prev = Prev->right;
}
Prev->right = Right;
root->left = nullptr;
// Continue processing right tree.
flatten(root->right);
}
};
其实这个递归方法非常容易想到,就是每次退栈的时候把左子树嫁接到右子树上。也很容易用非递归方法改写。但是 \(\mathrm O(1)\) 的思路我确实没见过。
class Solution {
public:
void flatten(TreeNode* root) {
TreeNode *Curr = root;
while (Curr) {
TreeNode *Left = Curr->left;
if (Left) {
TreeNode *Pred = Left;
while (Pred->right)
Pred = Pred->right;
// Move successors onto left rightmost branch.
Pred->right = Curr->right;
// Move the whole left branch onto right.
Curr->left = nullptr;
Curr->right = Left;
}
// Already moved onto right. We can safely progress now.
Curr = Curr->right;
}
}
};
这个思路是根据莫里斯遍历改写的,它是一种可以实现 \(\mathrm O(1)\) 空间的遍历方法——其实就是利用叶子结点的空余指针构造线索二叉树。它在算法当中是有一席之地的。
这种解法更狠,压根就不需要等什么退栈不退栈的,人家直接利用前序遍历「左子树的最右节点是右子树的前驱」的性质,在莫里斯遍历的基础上,不是创建线索,而是把右子树直接拉下来。记代码属实是不太好记,但是力扣官方题解里给的动图倒是挺直观的,可以用这个记一下。
还有一种仍然采用递归的 \(\mathrm O(n)\) 空间遍历是反前序遍历,如果在这种遍历的途中输出结果的话,最后的结果就和前序遍历完全相反。假设前序遍历顺序是
$$ 1\ 2\ 3\ 4\ 5\ 6 $$那么它的遍历顺序自然也就是
$$ 6\ 5\ 4\ 3\ 2\ 1 $$
这种遍历采用回溯的思路——第一次遍历到的节点是前序遍历中最后的节点 \(6\),那么它就是所有节点的后缀节点,我们可以把它记录在 Suffix 变量当中;第二次遍历到的是 \(5\),那么我们很自然地就可以形成 \(5\to6\) 这种链条,并且把 Suffix 赋值为 \(5\)……以此类推,最终得到 \(1\to2\to3\to4\to5\to6\) 的完整链条,并返回 Suffix。
class Solution {
TreeNode *Suffix{};
public:
void flatten(TreeNode* root) {
if (!root)
return;
flatten(root->right);
flatten(root->left);
root->left = nullptr;
root->right = Suffix;
Suffix = root;
}
};
反前序遍历最大的特点在于,它事实上是一种后序遍历,而后序遍历的计算能力其实是 \(\mathrm{LR}(0)\) 级别的!所以我们很自然地就获取了比单纯的前序遍历多得多的信息,也就不至于像我写的前序一样这么麻烦。
这个确实太优雅了,我靠。两种都好优雅,一种是空间 \(\mathrm O(1)\) 的优雅,一种是递归可计算性的优雅。
三刷
class Solution {
public:
void flatten(TreeNode* root) {
TreeNode *This = root;
while (This) {
TreeNode *Left = This->left;
if (Left) {
while (Left->right)
Left = Left->right;
Left->right = This->right;
// The following two statements are critical. Especially the assign-to-
// nullptr one. This prevents multiple reference.
This->right = This->left;
This->left = nullptr;
}
This = This->right;
}
}
};
其实没什么好说的,图示能画明白就可以了。但是注意 This->left = nullptr; 这一句不能漏,否则会产生重复引用而导致资源释放出现问题。
四刷
class Solution {
public:
void flatten(TreeNode* root) {
if (!root)
return;
TreeNode *Batch = root;
// Only one layer of loop is enough.
while (Batch) {
if (Batch->left) {
TreeNode *Cont = Batch->right;
Batch->right = Batch->left;
Batch->left = nullptr;
TreeNode *Dispatch = Batch->right;
// Connect the thread that's been cut.
while (Dispatch->right)
Dispatch = Dispatch->right;
Dispatch->right = Cont;
}
Batch = Batch->right;
}
}
};
其实这种解法的关键就在于while (Batch)只需要一层而不需要更多层,结果就是得到了\(\mathrm O(n)\)的时间和\(\mathrm O(1)\)的空间,这也是利用了树作为递归数据结构的特性。