二叉树转单链表
leetcode 114. 二叉树展开为链表
https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/
给定一个二叉树,原地将它展开为一个单链表。
例如,给定二叉树
1
/ \
2 5
/ \ \
3 4 6
将其展开为:
1
\
2
\
3
\
4
\
5
\
6
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void flatten(TreeNode* root) {
}
};
递归解法
可以发现展开的顺序其实就是二叉树的前序遍历。
- 所以最简单的方式是,用前序遍历的方式遍历这棵树,将遍历结果依次保存到一个容器中,最后把这个容器中的每个元素串成单链表即可。这种方式简单是简单,但空间复杂度肯定不是很好。
这种就不写了,就是基本的前序框架,把输出节点值,改为像容器尾插入当前节点指针即可。最后遍历容器,串成单链表。
时间复杂度:O(N)
空间复杂度:O(N)
- 不用容器保存所有遍历结果的方式,直接改造二叉树前序遍历的框架
内容来自:https://leetcode.wang/leetcode-114-Flatten-Binary-Tree-to-Linked-List.html
只是下面改为C++实现
题目其实就是将二叉树通过右指针,组成一个链表。
1 -> 2 -> 3 -> 4 -> 5 -> 6
我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们能不能利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。
先序遍历的顺序是1 2 3 4 5 6。
遍历到2,把1的右指针指向2。1 -> 2 3 4 5 6。
遍历到3,把2的右指针指向3。1 -> 2 -> 3 4 5 6。
… …
但是我们把1的右指针指向2,那么1的原本的右孩子就丢失了,也就是5 就找不到了。
解决方法的话,我们可以逆过来进行。
我们依次遍历6 5 4 3 2 1,然后每遍历一个节点就将当前节点的右指针更新为上一个节点。
遍历到5,把5的右指针指向6。6 <- 5 4 3 2 1。
遍历到4,把4的右指针指向5。6 <- 5 <- 4 3 2 1。
… …
1
/ \
2 5
/ \ \
3 4 6
这样就不会有丢失孩子的问题了,因为更新当前的右指针的时候,当前节点的右孩子已经访问过了。
而6 5 4 3 2 1的遍历顺序其实变形的后序遍历,遍历顺序是右子树->左子树->根节点
改造一下后序遍历的框架,同时利用一个全局变量pre,更新当前根节点的右指针为pre,左指针为null。
TreeNode* pre=nullptr;
//后序遍历的变形 “右左根”
void flatten(TreeNode* root) {
if(root==nullptr) return ;
flatten(root->right);
flatten(root->left);
//当前节点跟前一个结点连起来
root->right=pre;
//当前节点left置nullptr
root->left=nullptr;
//保存当前节点
pre=root;
}
相应的左孩子也要置为null,同样的也不用担心左孩子丢失,因为是后序遍历,左孩子已经遍历过了。
后序遍历改为迭代实现如下:
待续
前面提到如果用前序遍历的话,会丢失掉右孩子,除了用后序遍历,还有没有其他的方法避免这个问题。
我们知道利用栈,可以将递归的前序遍历改为迭代实现。
由于栈是先进后出,所以我们先将右节点入栈。提前将右孩子保存到栈中,这种遍历方式就可以防止右孩子的丢失了。
//前序遍历的迭代实现
void flatten(TreeNode* root) {
if(root==nullptr) return ;
stack<TreeNode*> s;
s.push(root);
TreeNode* pre=nullptr;
while(!s.empty()){
TreeNode* temp=s.top();
s.pop();
if(temp->right!=nullptr)
s.push(temp->right);
if(temp->left!=nullptr)
s.push(temp->left);
if(pre!=nullptr){
pre->right=temp;
pre->left=nullptr;
}
pre=temp;
}
}
非递归解法
- 将左子树插入到右子树的地方
- 将原来的右子树接到左子树的最右边节点
- 考虑新的右子树的根节点,一直重复上边的过程,直到新的右子树为 nullptr
1
/ \
2 5
/ \ \
3 4 6
//将 1 的左子树插入到右子树的地方
1
\
2 5
/ \ \
3 4 6
//将原来的右子树接到左子树的最右边节点
1
\
2
/ \
3 4
\
5
\
6
//将 2 的左子树插入到右子树的地方
1
\
2
\
3 4
\
5
\
6
//将原来的右子树接到左子树的最右边节点
1
\
2
\
3
\
4
\
5
\
6
......
void flatten(TreeNode* root) {
while(root!=nullptr){
if(root->left!=nullptr){
//找到左子树的最右边节点
TreeNode* temp=root->left;
while(temp->right)
temp=temp->right;
//将原来的右子树接到左子树的最右边节点
temp->right=root->right;
// 将左子树插入到右子树的地方
root->right=root->left;
root->left=nullptr;
//root移到下一个节点,等待下次循环分析
root=root->right;
}else{
root=root->right;
}
}
}
分治思想:
- 先把root的左子树转化为单向链表,表头指针left,再把右子树转化为单向链表,表头指针right
- 然后把left指向的单链表,right指向的单链表,root,三者连接起来。
具体为令root->left=nullptr, root->right=left,然后找到左子树转化后的单链表的尾指针leftTail,最后令leftTail->right=right;
定义一个函数,将root指向的二叉树转化为单链表,返回转化后单链表的表头
TreeNode* binaryTreeToList(TreeNode* root);
代码实现上看,分治思想有点后序遍历的感觉
class Solution {
public:
TreeNode* binaryTreeToList(TreeNode* root){
//base case
if(root==nullptr) return nullptr;
TreeNode* leftTail=nullptr;
//分
TreeNode* left=binaryTreeToList(root->left);
TreeNode* right=binaryTreeToList(root->right);
//治
//根据left是否为nullptr,
//将left,right,root连接起来的操作不同
if(left!=nullptr){
leftTail=left;
//找到left指向的单链表最后一个指针
while(leftTail->right)
leftTail=leftTail->right;
root->right=left;
root->left=nullptr;
leftTail->right=right;
}else{
root->left=nullptr;
root->right=right;
}
return root;
}
void flatten(TreeNode* root) {
binaryTreeToList(root);
}
};
二叉树转双向链表
《剑指offer》面试题36
输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建新的结点,只能调整树中结点指针的指向。
递归实现
- 很明显的中序遍历框架,根据上面的类似的分析,就是把中序遍历的序列连接起来,只是节点的左右指针都得连接起来,同时,必须先找到最左节点作为表头返回。
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
if(pRootOfTree==nullptr) return nullptr;
//找到转换后的头部,即最左节点
TreeNode* cur=pRootOfTree;
while(cur->left)
cur=cur->left;
//中序遍历将二叉搜索树转双向链表
inorderRecursion(pRootOfTree);
//返回转换后的双向链表头
return cur;
}
//中序遍历框架
TreeNode* pre=nullptr;
void inorderRecursion(TreeNode* root){
if(root==nullptr) return ;
inorderRecursion(root->left);
root->left=pre;
//记住访问指针指向的空间中的元素时,得先判断指针非nullptr
if(pre!=nullptr)
pre->right=root;
pre=root;
inorderRecursion(root->right);
}
};
- 根据中序遍历,上面利用一个辅助函数void inorderRecursion(TreeNode* root);来实现,主要要用一个外部变量TreeNode* pre,来保存中序遍历序列的前一个结点。
如果直接用题目提供的接口函数TreeNode* Convert(TreeNode* pRootOfTree);,也可以利用中序遍历框架来实现,只是不像上面那样好理解。可以先看看下面的分治思想,应该有助于理解这里的代码。
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
if(pRootOfTree==nullptr) return nullptr;
TreeNode* temp=nullptr;
//left为左子树转换成双向链表后的链表头
TreeNode* left=Convert(pRootOfTree->left);
//根据left是否为nullptr,将left与pRootOfTree连接起来
if(left!=nullptr){
temp=left;
while(temp->right)
temp=temp->right;
temp->right=pRootOfTree;
pRootOfTree->left=temp;
}else{
pRootOfTree->left=nullptr;
}
//right为右子树转换成双向链表后的链表头
TreeNode* right=Convert(pRootOfTree->right);
//根据right是否为nullpr,将pRootOfTree与right连接起来
if(right!=nullptr)
right->left=pRootOfTree;
pRootOfTree->right=right;
//根据left是否为nullptr,决定最终返回
if(left!=nullptr)
return left;
else
return pRootOfTree;
}
};
分治思想
用分治思想分析这题
- 先把root的左子树转化为双向链表,表头指针left,再把右子树转化为双向链表,表头指针right
- 然后把left指向的双向链表,right指向的双向链表,root,三者连接起来。
具体为
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
if(pRootOfTree==nullptr) return nullptr;
TreeNode* temp=nullptr;
//分
TreeNode* left=Convert(pRootOfTree->left);
TreeNode* right=Convert(pRootOfTree->right);
//治
//根据left是否为nullptr,具体连接操作不一样
if(left!=nullptr){
//先找到左子树转换成双向链表后的尾节点
temp=left;
while(temp->right)
temp=temp->right;
temp->right=pRootOfTree;
pRootOfTree->left=temp;
pRootOfTree->right=right;
if(right!=nullptr)
right->left=pRootOfTree;
return left;
}else{
pRootOfTree->left=nullptr;
pRootOfTree->right=right;
if(right!=nullptr)
right->left=pRootOfTree;
return pRootOfTree;
}
}
};