题目
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
思路
首先我们需要对题目进行解读,对于双向链表,我们可以理解为在单向链表拥有next指针的基础上再添加了一个指向前面节点的指针pre
,如下所示:
然后我们需要对二叉搜索树有一个了解。二叉搜索树是在二叉树的基础上其中节点的值满足cur.left.value<cur.value<cur.right.value
这样一个关系,其中cur
为当前节点的位置。如下图就是一个标准的二叉搜索树
这题的要求有两点
- 不能创建新的节点
- 转换后的双向链表是一个升序排序的
通过关键信息排序我们可以从二叉树的三种遍历思想出发,其中可以发现对于二叉搜索树进行中序遍历得到的结果正好满足升序排列。
对于二叉树的遍历我们可以这么理解X序遍历,这里的X就是说在遍历一个最基本的二叉树的时候根节点的遍历顺序
中序遍历:左子树>>根节点>>右子树
先序遍历:根节点>>左子树>>右子树
后序遍历:左子树>>右子树>>根节点
本题的解题方法有以下三种:
- 先中序遍历二叉树,将输出的节点保存在一个
ArrayList
中,然后对ArrayList
中的节点进行前后关系的创建 - 利用二叉树线索化的过程思想,我们这里也可以提出一个全局变量
pre
,一开始给pre
初始化为空,然后对二叉树进行中序遍历的时候,从叶子节点出发,只要pre
不为空就创建关系,这样就可以生成双向链表了 - 对上面方法的优化
方法一
我们需要定义一ArrayList
,然后将中序遍历二叉树后的节点依次保存进ArrayList
中,然后还需要一个函数将ArrayList
中的独立的节点建立起前后的联系,最后输出ArrayList
中的头节点即可。
import java.util.ArrayList
//定义一个二叉树的类
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
public TreeNode Convert(TreeNode pRootOfTree) {
if(pRootOfTree == null){
return null;
}
ArrayList<TreeNode> list = new ArrayList<>();
Convert(pRootOfTree, list);
return Convert(list);
}
//中序遍历,在list中按遍历顺序保存
public void Convert(TreeNode pRootOfTree, ArrayList<TreeNode> list){
if(pRootOfTree.left != null){
Convert(pRootOfTree.left, list);
}
list.add(pRootOfTree);
if(pRootOfTree.right != null){
Convert(pRootOfTree.right, list);
}
}
//遍历list,修改指针
public TreeNode Convert(ArrayList<TreeNode> list){
for(int i = 0; i < list.size() - 1; i++){
list.get(i).right = list.get(i + 1);
list.get(i + 1).left = list.get(i);
}
return list.get(0);
}
该方法较容易想到,是将题目的整体需求拆分为两部分分别进行求解的,由于多了一个链表关系建立的方法,所以时间复杂度会高一些
方法二
通过上面的法一我们可以知道线性二叉树的中序遍历输出的结果是满足我们题目要求的,但是还需要在对输出的ArrayList
进行一次遍历建立节点的前后关联。我们想是否可以在中序遍历的时候就直接完成双向链表的前后节点关系的建立。
1.通过多层递归直接定位到二叉树的左子树的最左叶子节点位置,并且将节点指针pre
和root
指向该位置
- 此时的
root
和pre
都不为空,让后移动pre
指针,调整节点之间的关系
3.重复步骤二的行为,
最后输出root
处的节点就可以了
public class Solution {
TreeNode pre=null;
TreeNode root=null;
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree==null)
return null;
Convert(pRootOfTree.left);
if (root==null){
root=pRootOfTree;
}
if (pre!= null){
pRootOfTree.left=pre;
pre.right=pRootOfTree;
}
pre=pRootOfTree;
Convert(pRootOfTree.right);
return root;
}
}
该方法在法一的基础上进行优化,相对于法一,它没有开辟新的ArrayList
去暂存节点,节省了空间;其二,他在进行中序遍历的时候就直接将节点的关系调整好了,不需要进一步的遍历ArrayList
去调整节点关系,节省时间。
方法三
上面的方法二之所以需要用一个root
暂存左子树的最左叶子节点的原因在于,我们的调整指针pre
最后指向的是右子树的最右叶子节点,如果不用root
暂存的话,会需要花费时间对形成的双向链表进行一次反向遍历。之所以会出现上面的情况是因为我们采用的中序遍历,如过我们采用后序遍历,那么pre
指针一开始指向的就是右子树的最右叶子节点,随着遍历,最后直接指向的是左子树的最左叶子节点。
public class Solution {
TreeNode pre=null;
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree==null)
return null;
Convert(pRootOfTree.right);
if (pre!= null){
pRootOfTree.right=pre;
pre.left=pRootOfTree;
}
pre=pRootOfTree;
Convert(pRootOfTree.left);
return pre;
}
}
改方法大体思路跟方法二一致,只不过通过改变二叉树的遍历方式去掉了root
指针,看起来更简洁
总结
对于方法一是常规方法比较容易想到,就是要注意如果面试的时候考官问你有没有优化的时候,这个时候我们就可以从这个题目的优化角度出发。如果原先一个问题被拆分为两个相互独立的简单问题的时候,优化的思路就是在两个独立的方法中找到一个遍历的方法,然后看能不能将另外一个方法在一边遍历的时候就直接解决掉,这样就可以将两个问题合并为一个问题解决。