前言:今天在解决一个问题时,程序总是不能输出正确值,分析逻辑思路没问题后,发现原来是由于函数传递导致了这个情况。

LeetCode 113

问题:给你二叉树的根节点root和一个整数目标和targetSum,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

示例

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

我的代码如下

 1 class Solution {
2 public void traversal(TreeNode root, int count, List<List<Integer>> res, List<Integer> path) {
3 path.add(root.val);
4 if (root.left == null && root.right == null) {
5 if (count - root.val == 0) {
6 res.add(path);
7 }
8 return;
9 }
10 ​
11 if (root.left != null) {
12 traversal(root.left, count - root.val, res, path);
13 path.remove(path.size() - 1);
14 }
15 if (root.right != null) {
16 traversal(root.right, count - root.val, res, path);
17 path.remove(path.size() - 1);
18 }
19 }
20 ​
21 public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
22 List<List<Integer>> res = new ArrayList<>();
23 List<Integer> path = new ArrayList<>();
24 if (root == null) return res;
25 traversal(root, targetSum, res, path);
26 ​
27 return res;
28 }
29 }

该题的思路是采用递归,traversal函数内root是当前树的根节点,count是目标值,res是存储结果,path是路径。该代码对于示例的输入输出为

1 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
2 输出:[[5],[5]]

经过排查最终问题在于代码中的add方法

原代码部分内容为

1 if (root.left == null && root.right == null) {
2 if (count - root.val == 0) {
3 res.add(path);
4 }
5 return;
6 }

该部分内容需要改为

1 if (root.left == null && root.right == null) {
2 if (count - root.val == 0) {
3 res.add(new ArrayList(path));
4 }
5 return;
6 }

此时所有代码对于示例的输入输出为

1 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
2 输出:[[5,4,11,2],[5,8,4,5]]

在java中,存在8大基本数据类型,且均有对应的包装类

数据类型 占用位数 默认值 包装类
byte(字节型) 8 0 Byte
short(短整型) 16 0 Short
int(整型) 32 0 Integer
long(长整型) 64 0.0l Long
float(浮点型) 32 0.0f Float
double(双精度浮点型) 64 0.0d Double
char(字符型) 16 "/u0000" Character
boolean(布尔型) 1 false Boolean

在java中,函数传递只有值传递,是指在调用函数时,将实际参数复制一份传递给函数,这样在函数中修改参数(形参)时,不会影响到实际参数。

基本数据类型的值传递

测试类

 1 public class TestClass {
2 public static void test(int value) {
3 value = 2;
4 System.out.println("形参value的值:" + value);
5 }
6 ​
7 public static void main(String[] args) {
8 int value = 1;
9 System.out.println("调用函数前value的值:" + value);
10 test(value);
11 System.out.println("调用函数后value的值:" + value);
12 }
13 }

结果为

1 调用函数前value的值:1
2 形参value的值:2
3 调用函数后value的值:1

结论:可以看到,int类型的value初始为1,调用函数后,value仍然为1,基本数据类型在函数中修改参数(形参)时不会影响到实参的值。

引用数据类型的值传递

类TreeNode

 1 public class TreeNode {
2 int val;
3 TreeNode left;
4 TreeNode right;
5 ​
6 TreeNode() {
7 }
8 ​
9 TreeNode(int val) {
10 this.val = val;
11 }
12 ​
13 TreeNode(int val, TreeNode left, TreeNode right) {
14 this.val = val;
15 this.left = left;
16 this.right = right;
17 }
18 }

测试类1

 1 public class TestClass {
2 public static void test(TreeNode node) {
3 node.val = 2;
4 System.out.println("形参node的val值:" + node.val);
5 }
6 ​
7 public static void main(String[] args) {
8 TreeNode node = new TreeNode(1);
9 System.out.println("调用函数前node的val值:" + node.val);
10 test(node);
11 System.out.println("调用函数后node的val值:" + node.val);
12 }
13 }

结果为

1 调用函数前node的val值:1
2 形参node的val值:2
3 调用函数后node的val值:2

结论:可以看到,TreeNode类型的node对象的val值初始为1,调用函数后,node对象的val值被修改为2,引用数据类型在函数中修改参数(形参)时影响到了实参的值。

现在看另一个示例

测试类2

 1 public class TestClass {
2 public static void test(TreeNode node) {
3 node = new TreeNode(2);
4 System.out.println("形参node的val值:" + node.val);
5 }
6 ​
7 public static void main(String[] args) {
8 TreeNode node = new TreeNode(1);
9 System.out.println("调用函数前node的val值:" + node.val);
10 test(node);
11 System.out.println("调用函数后node的val值:" + node.val);
12 }
13 }

结果为

1 调用函数前node的val值:1
2 形参node的val值:2
3 调用函数后node的val值:1

结论:可以看到,TreeNode类型的node对象的val值初始为1,调用函数后,node对象的val值仍然为1,引用数据类型在函数中修改参数(形参)时未影响到实参的值。

那么,为什么会出现这种问题呢?

首先,在JAVA中,函数传递都是采用值传递,实际参数都会被复制一份给到函数的形式参数,所以形式参数的变化不会影响到实际参数,基本数据类型的值传递示例可以发现这个性质。但引用数据类型的值传递为什么会出现修改形式参数的值有时会影响到实际参数,而有时又不会影响到实际参数呢?其实引用数据类型传递的内容也会被复制一份给到函数的形式参数,这个内容类似C++中的地址,示例中的node对象存储于堆中,虽然形参与实参是两份内容,但内容值相同,都指向堆中相同的对象,故测试类1在函数内修改对象值时,函数外查看时会发现对象值已被修改。测试类2在函数内重新构造了一个对象node,在堆中申请了一个新对象(新对象与原对象val值不相同),让形参指向这个对象,所以不会影响到原对象node的值。测试类1与测试类2的区别在于引用数据类型的指向对象发生了变化。

以下代码可验证上述分析

测试类1

 1 public class TestClass {
2 public static void test(TreeNode node) {
3 System.out.println("test:node" + node);
4 node.val = 2;
5 System.out.println("test:node" + node);
6 System.out.println("形参node的val值:" + node.val);
7 }
8 ​
9 public static void main(String[] args) {
10 TreeNode node = new TreeNode(1);
11 System.out.println("调用函数前node的val值:" + node.val);
12 System.out.println("main node:" + node);
13 test(node);
14 System.out.println("调用函数后node的val值:" + node.val);
15 System.out.println("main node:" + node);
16 }
17 }

结果为

1 调用函数前node的val值:1
2 main node:TreeNode@1540e19d
3 test:nodeTreeNode@1540e19d
4 test:nodeTreeNode@1540e19d
5 形参node的val值:2
6 调用函数后node的val值:2
7 main node:TreeNode@1540e19d

测试类2

 1 public class TestClass {
2 public static void test(TreeNode node) {
3 System.out.println("test:node" + node);
4 node = new TreeNode(2);
5 System.out.println("test:node" + node);
6 System.out.println("形参node的val值:" + node.val);
7 }
8 ​
9 public static void main(String[] args) {
10 TreeNode node = new TreeNode(1);
11 System.out.println("调用函数前node的val值:" + node.val);
12 System.out.println("main node:" + node);
13 test(node);
14 System.out.println("调用函数后node的val值:" + node.val);
15 System.out.println("main node:" + node);
16 }
17 }

结果为

1 调用函数前node的val值:1
2 main node:TreeNode@1540e19d
3 test:nodeTreeNode@1540e19d
4 test:nodeTreeNode@677327b6
5 形参node的val值:2
6 调用函数后node的val值:1
7 main node:TreeNode@1540e19d

对于测试类1,形参和实参都是指向相同的对象,所以利用形参修改对象的值,实参指向的对象的值发生改变。对于测试类2,形参在函数开始和实参指向相同的对象,让其指向新的对象后,实参指向的对象的值不会发生改变。简要说,测试类1形参复制了实参的地址,修改了地址对应的对象值,但并未修改地址值,测试类2形参复制了实参的地址,并修改了地址值,但并未修改原地址值对应的对象值。


有了目前的结论,可以理解为什么res.add()函数内path修改为new ArrayList(path)就可代码运行成功。因为我的path类型为List<Integer>,为引用数据类型,且path的值一直在发生变化。随着递归代码的运行,path的值发生变化,res内最初的List<Integer>值会发生变化(就是path的值)。但将path修改为new ArrayList(path)后,是在堆中新构造了对象,并指向该对象,原对象的变化不会影响到该对象的值,那么res内List<Integer>值就不会发生变化。

listList.add()方法直接传入list1

 1 import java.util.ArrayList;
2 import java.util.List;
3 ​
4 public class TestClass {
5 public static void main(String[] args) {
6 List<List<Integer>> listList = new ArrayList<>();
7 List<Integer> list1 = new ArrayList<>();
8 list1.add(1);
9 listList.add(list1); //直接add list1
10 List<Integer> list2 = new ArrayList<>();
11 list2.add(2);
12 listList.add(list2);
13 System.out.println("list1改变前");
14 for (List<Integer> l : listList) {
15 for (Integer i : l) {
16 System.out.println(i);
17 }
18 System.out.println("---");
19 }
20 list1.set(0, 2); //将list1的0号元素改为2
21 System.out.println("list1改变后");
22 for (List<Integer> l : listList) {
23 for (Integer i : l) {
24 System.out.println(i);
25 }
26 System.out.println("---");
27 }
28 }
29 }

结果为

 1 list1改变前
2 1
3 ---
4 2
5 ---
6 list1改变后
7 2
8 ---
9 2
10 ---

listList.add()方法重新构造新对象(内容与list1相同)

 1 import java.util.ArrayList;
2 import java.util.List;
3 ​
4 public class TestClass {
5 public static void main(String[] args) {
6 List<List<Integer>> listList = new ArrayList<>();
7 List<Integer> list1 = new ArrayList<>();
8 list1.add(1);
9 listList.add(new ArrayList<>(list1)); //构造新对象 再调用add
10 List<Integer> list2 = new ArrayList<>();
11 list2.add(2);
12 listList.add(list2);
13 System.out.println("list1改变前");
14 for (List<Integer> l : listList) {
15 for (Integer i : l) {
16 System.out.println(i);
17 }
18 System.out.println("---");
19 }
20 list1.set(0, 2); //将list1的0号元素改为2
21 System.out.println("list1改变后");
22 for (List<Integer> l : listList) {
23 for (Integer i : l) {
24 System.out.println(i);
25 }
26 System.out.println("---");
27 }
28 }
29 }

结果为

 1 list1改变前
2 1
3 ---
4 2
5 ---
6 list1改变后
7 1
8 ---
9 2
10 ---

结论:调用构造函数后,函数指向新的对象,原对象的值发生改变,函数内值也不会改变。同理,新对象的值发生改变,原对象的值也不会发生改变。

注意!JAVA中的值传递的更多相关文章

  1. Java中的值传递

    1.先比较下字符串的比较 == 代表全等于 值和地址(存放地址) 全部相等于. equals 值等于== 和 equals的区别 列如下面的 如果name1==name2是等于的 然而name1==n ...

  2. 为什么说Java中只有值传递

    本文转载自公众号 Hollis 对于初学者来说,要想把这个问题回答正确,是比较难的.在第二天整理答案的时候,我发现我竟然无法通过简单的语言把这个事情描述的很容易理解,遗憾的是,我也没有在网上找到哪篇文 ...

  3. 一道笔试题来理顺Java中的值传递和引用传递

      题目如下: private static void change(StringBuffer str11, StringBuffer str12) { str12 = str11; str11 = ...

  4. java中的值传递和引用传递有什么区别呀?

    值传递: (形式参数类型是基本数据类型和String):方法调用时,实际参数把它的值传递给对应的形式参数,形式参数只是用实际参数的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形式参 ...

  5. 为什么说Java中只有值传递(转载)

    出处:https://www.hollischuang.com/archives/2275 关于这个问题,在StackOverflow上也引发过广泛的讨论,看来很多程序员对于这个问题的理解都不尽相同, ...

  6. 为什么说Java中只有值传递?

    一.为什么说Java中只有值传递? 对于java中的参数传递方式中是否有引用传递这个话题,很多的人都认为Java中有引用传递,但是我个人的看法是,Java中只有值传递,没有引用传递. 那么关于对象的传 ...

  7. 为什么说Java中只有值传递----说服自己

    在开始深入讲解之前,有必要纠正一下大家以前的那些错误看法了.如果你有以下想法,那么你有必要好好阅读本文. 错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递.如果是个引用,就 ...

  8. Java 中的值传递和引用传递问题

    Java 中的值传递和引用传递问题 public class Operation { int data = 50; void change(int data) { data = data + 100; ...

  9. Java中只有值传递,(及值传递与引用传递详解)

    首先呢,我们来说一下值传递与引用传递的区别(这两个玩意儿实在调用函数的时候提到的) 比如说 code( a) code( int a ) code(a)是调用函数,a是我们原本函数的一个值类型,然后使 ...

  10. 为什么说 Java 中只有值传递?

    对于初学者来说,要想把这个问题回答正确,是比较难的.在第二天整理答案的时候,我发现我竟然无法通过简单的语言把这个事情描述的很容易理解,遗憾的是,我也没有在网上找到哪篇文章可以把这个事情讲解的通俗易懂. ...

随机推荐

  1. 分享Zeal的全套离线文档

    鉴于Zeal自身的下载速度... 为了方便大家,现在把我自己下载好的Zeal离线文档全部分享出来 百度网盘链接:https://pan.baidu.com/s/19WeEWij3evnuMWhzbHu ...

  2. 2022-07-11:给定n位长的数字字符串和正数k,求该子符串能被k整除的子串个数。 (n<=1000,k<=100)。 来自微众。4.11笔试。

    2022-07-11:给定n位长的数字字符串和正数k,求该子符串能被k整除的子串个数. (n<=1000,k<=100). 来自微众.4.11笔试. 答案2022-07-11: 动态规划. ...

  3. Django中多个app放置同一文件夹中

    在pycharm中新建一个管理app的python package目录:apps 将存在的app用拖拽到apps目录下,此时会弹出对话框,取消勾选Search for references(搜索索引) ...

  4. 知识拷问:工作站和服务器哪个更适合做CST电磁仿真?

    通常大型企业都会具备工作站和服务器用以作为办公的支持,在大家做仿真分析时,我们一般建议大家更多地使用工作站,工作站要比服务器更适合做CST软件的仿真运算. 什么是服务器? 服务器是指在网络环境下运行相 ...

  5. 【Java】包名规范及整理

    目录 前言 包名规范 总结 前言 最近学习Java的时候,有一个 class 需要在每一个 java文件中写一写,然后我喜欢一次实验的java文件放到一个 Package 中,这就导致了持续不断的报错 ...

  6. 用Linux命令操作mysql数据库

    操作mysql数据库,相信大家最熟悉的应该是用navicat工具来新建数据库,建表,查询数据,查看表结构等. 但是如果数据库与本操作机器不在同一个局域网内,并且对方环境也不支持vpn的情况下,如何查询 ...

  7. RabbitMQ系列-概念及安装

    1. 消息队列 消息队列是指利用队列这种数据结构进行消息发送.缓存.接收,使得进程间能相互通信,是点对点的通信 而消息代理是对消息队列的扩展,支持对消息的路由,是发布-订阅模式的通信,消息的发送者并不 ...

  8. GIT使用的记录

    使用的是win7下的git 安装就不赘述了下面的文档说的很详细 Git客户端图文详解如何安装配置GitHub操作流程攻略 http://www.ihref.com/read-16377.html 到下 ...

  9. 泰裤辣!!!手摸手教学,如何训练一个你的专属AI歌姬~

    最近在做AIGC的项目,不过是与图片相关的,现在的模型效果可比前几年图片替换效果好多了.之前尝试过用 faceswap 工具来进行人脸替换的,具体可以参看下我之前的这篇文章:https://blog. ...

  10. R画韦恩图之总结

    本文分享自微信公众号 - 生信科技爱好者(bioitee).如有侵权,请联系 support@oschina.cn 删除.本文参与"OSC源创计划",欢迎正在阅读的你也加入,一起分 ...