给定一个二叉树,原地将它展开为链表。
例如,给定二叉树
1 / \ 2 5 / \ \ 3 4 6
将其展开为:
1 \ 2 \ 3 \ 4 \ 5 \ 6
思路:
这道题有递归和非递归两种做法。
递归:由于最后链表的排序可以看出是前序遍历的结果,所以首先一直循环访问左节点直到为空,然后把左节点插到父节点和右节点之间,依次遍历所有节点。
void flattenCore(TreeNode* root, TreeNode* parent) {
if (!root) {
return;
}
flattenCore(root->left, root);
if (parent->left) {
TreeNode* tmp = parent->left;
while(tmp->right){
tmp=tmp->right;
}
tmp->right=parent->right;
parent->right=parent->left;
parent->left=nullptr;
}
flattenCore(root->right, root);
}
void flatten(TreeNode* root) {
if (!root) {
return;
}
flattenCore(root, root);
}
这里解释一下为什么代码不是前序遍历的模板代码:
//业务代码
//........
//左节点
flattenCore(root->left, root);
//右节点
flattenCore(root->right, root);
按道理说前序遍历应该把打印节点或者处理逻辑放到左节点递归之前。为什么这个代码写的像中序遍历呢,如下图?
//左节点
flattenCore(root->left, root);
//业务代码
//........
//右节点
flattenCore(root->right, root);
因为我们的任务不是打印节点,不是按照访问的顺序遍历节点,而应该考虑最后的拼接结果。如果按照前序遍历处理业务逻辑,也可以做,但是代码需要修改,不利于思考(非递归代码可以用前序遍历的思想),我们希望从树的最左边开始逐个归并到右节点,这样当访问到根节点时,左节点的树已经归并到右子树了,并且对左节点的所有节点而言已经是链表的形状了。
代码如下:
void flattenCore(TreeNode* root, TreeNode* parent) {
if (!root) {
return;
}
flattenCore(root->left, root);
if (parent->left) {
TreeNode* tmp = parent->left;
while(tmp->right){
tmp=tmp->right;
}
tmp->right=parent->right;
parent->right=parent->left;
parent->left=nullptr;
}
flattenCore(root->right, root);
}
void flatten(TreeNode* root) {
if (!root) {
return;
}
flattenCore(root, root);
}
上述递归有个重复递归的过程,如下树:
1 / \ 2 5 / \ \ 3 4 6
我们递归节点3时,当把3归并到2~4之间时,结果如下图:
1 / \ 2 5 \ \ 3 6 \ 4
这时递归3->right,由于4只有一个节点,返回不变。
然后递归2节点:
1 / \ 2 5 \ \ 3 6 \ 4
1 \ 2 \ 3 \ 4 \ 5 \ 6
由于2节点是1->left,然后递归1->right,这时发现1->right会包含已经递归排序好的一部分1->left的内容,所以相当于每次递归右子树的时候,会把左子树再递归一遍,虽然不处理业务逻辑但是确实在消耗时间。所以优化的方法有如下的非递归方法。
非递归:
类似“前序排列”的思想,如果当前节点有左子节点,就把左子节点插到当前节点和右节点之间,然后当前节点更新为右节点,重复以上过程。
void flatten(TreeNode* root) {
if (!root) {
return;
}
TreeNode* cur = root;
while (cur) {
if (cur->left) {
TreeNode* tmp = cur->left;
while (tmp->right) {
tmp = tmp->right;
}
tmp->right = cur->right;
cur->right = cur->left;
cur->left=nullptr;
}
cur = cur->right;
}
}
这样没有重复遍历,理论上效率会更高一些