前言
今天在做这道LeetCode题目的时候,在参考其中一个解题思路的时候,把它对应的C++代码重写为java代码的时候,却无法通过。由此开始了一系列的Debug和验证的过程,最终找到问题的原因,并改写代码,成功通过。先简单阐述问题在哪里:
- java中的函数参数的传递只有值传递,没有引用传递。对于基本数据类型来说传递的是值本身,对于其余的数组,或者自定义的类来说,传递的是地址值的拷贝
- 对于c++来说,函数参数值传递的时候,传递的是引用,也就是地址本身,而不是拷贝。
题意
给定一个二叉树,将其就地转换为单链表,单链表中的顺序为前序遍历的顺序,且链表中的每个节点应该通过右子树相连。
即将上述的二叉树转换为:
思路
思路1
前序遍历整个二叉树,遍历的过程中将每一个节点保存在一个集合里,二叉树遍历完成,再遍历该集合,然后对于每一个节点(除最后一个节点),其左指针置null,右指针置下一个节点。这个思路直观,简单,但是不满足就地的条件。
思路2
同样是前序遍历,但是在遍历的时候想办法把每一个节点的左子树和右子树通过某种方法往右侧拉直,同时记录拉直后的左子树的最后一个节点位置,然后把其与拉直后的右子树的最后一个位置相连。
注意到上述的过程是在递归中进行的,也就是先递归寻找到一个left_last,然后与当前的right相连,然后再返回到上一层的进行同样的逻辑,这里就有一个很重要的事情在于left_last在递归返回的时候,应该返回到上一层作为和right连接使用。
理解了上述的思路,那么可以进一步细化一下递归函数的设计:
- 首先递归函数的输入应该是 当前遍历的节点node和一个用于记录最后一个节点的last,并且希望在递归返回的时候,last记录下了node节点下的left_last
- 进入递归函数,依次按下列进行:
- 1.判断当前节点是否为空,是则返回
- 2.判断当前节点是否为叶节点,若是则当当前节点赋值给last,并返回到上一层,同时把赋值后的last返回给上一层.
- 3.新建一对left,right备份当前节点的左子树和右子树,和left_last,right_last为空,作为下一次遍历的last传入.
- 4.如果左子树不为空,则进入下一次递归
- 4.1 递归结束,根据前面的条件,递归只可能在遍历到叶节点的时候返回,那么这个时候返回到上一层的时候,left_last记录的就是那个叶节点。
- 4.2将当前节点的左子树置空
- 4.3将当前节点的右子树置left
- 4.4更新当前层的last = left_last
- 5.如果右子树不为空,进入到下一次递归
- 5.1递归结束,根据前面的条件,递归只可能在遍历到叶节点的时候返回,那么这个时候返回到上一层的时候,right_last记录的就是那个叶节点。
- 5.2 如果left_last不为空,也就是有值,也就是前面在遍历左子树的时候遇到某个叶节点进行了返回,那么left_last->right = right,其实这一步就完成了一个拉直
- 5.3 更新当前层的last = right_last
错误代码示例
下面的代码是按照上述思路的第一次实现,就是出错的代码。
主函数
新建如图的二叉树---->调用函数完成拉直—>遍历输出
public static void main(String[] args) {
TreeNode node1 = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
TreeNode node6 = new TreeNode(6);
node1.left = node2;
node2.left = node3;
node2.right = node4;
node1.right = node5;
node5.right = node6;
//调用函数进行拉直
flatten(node1);
//遍历输出
TreeNode item = node1;
while (item != null){
System.out.println(item.val);
item = item.right;
}
}
拉直函数和递归函数
public static void flatten(TreeNode root) {
TreeNode last = null;
preorder(root, last);
}
public static void preorder(TreeNode node, TreeNode last){
if (node == null){
return;
}
//如果当前为叶节点
if (node.left == null && node.right == null){
//将当前节点置为最后的节点
last = node;
return;
}
//备份当前的左节点
TreeNode left = node.left;
TreeNode right = node.right;
TreeNode left_last = null;
TreeNode right_last = null;
if (left != null){
//如果左子树不为空,继续往后遍历
preorder(left, left_last);
//递归返回
node.left = null;
node.right = left;
last = left_last;
}
if (right != null){
preorder(right, right_last);
if (left_last != null){
left_last.right = right;
}
last = right_last;
}
}
Debug过程
第一层递归 node=1,node.left=2,准备进入下一层递归
第二层递归 node=2, node.left=3,准备进入下一层递归
第三层递归 node=3,为叶节点,进入下一层递归会返回
第三层返回到第二层,发现问题,last的信息没有被返回
- 那么问题就来了:为什么第三层的last信息没有如期返回呢?TreeNode传值的时候是一个对象,应该是按照引用传递,应该可以返回改动后的值,但是为什么没有呢?
- 原因就在于对于java中引用传值的方式理解得不够深入,导致的错误,下面将详细阐述:
Java中引用的值传递是以拷贝的方式进行的
下面看两个例子:
- 1.传递的时候拷贝了地址,通过地址访问对象,然后改变其值
代码:
public class ValueOrCite {
public static void main(String[] args) {
TreeNode a = new TreeNode(0);
System.out.println("Bfore change" + a.val);
changeValue(a);
System.out.println("After change" + a.val);
}
public static void changeValue(TreeNode x){
x.val = 1;
}
}
- 结果:
Bfore change0
After change1
很简单的一个例子,也很好懂,但是这个例子是通过地址去访问一个对象,并改变其值,和本题想要实现的效果是不一样的,本题是想在函数内部为一个对象赋值,然后将赋值后的结果传出去。
- 2.由于传参的时候是对对象地址做了一次拷贝,对该拷贝做赋值,是无法影响到原来的地址值的.
如图所示:
下面通过图像进一步解释:
函数传参过程:
函数执行过程:
那么如何解决这个问题呢?
暂时能想到的就只能通过改变函数返回值的方式,把改变后的x返回给函数外的a.
那么对应到这个题目,改变递归函数的返回值,把每一次递归返回的时候将last显示的返回出去
改变后的正确代码
public static TreeNode preorder02(TreeNode node, TreeNode last){
if (node == null){
return last;
}
//如果当前为叶节点
if (node.left == null && node.right == null){
//将当前节点置为最后的节点
last = node;
return last;
}
//备份当前的左节点
TreeNode left = node.left;
TreeNode right = node.right;
TreeNode left_last = null;
TreeNode right_last = null;
if (left != null){
//如果左子树不为空,继续往后遍历
left_last = preorder02(left, left_last);
// left_last = last;
//递归返回
node.left = null;
node.right = left;
last = left_last;
}
if (right != null){
right_last = preorder02(right, right_last);
if (left_last != null){
//如果左子树的最后节点不空 将其右孩子连接到当前节点
left_last.right = right;
}
//重置 last节点, 这是为了返回上一层递归做准备
last = right_last;
}
return last;
}
小结
- java的引用传递是一种拷贝的方式,如果一个函数需要改变某个引用参数的值(地址),那么一定要通过函数返回值的方式才能解决