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

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. Python Requets库学习总结

    快速开始 发送请求 >>> import requests >>> r = requests.get('https://api.github.com/events' ...

  2. vue3 + vite 多项目多模块打包

    vue3 + vite 多项目多模块打包 本示例基于vite-plugin-html插件,实现多个独立项目共存,共享组件和依赖,运行.打包互不干扰. npm create vite@latest 兼容 ...

  3. 2022-10-12:以下go语言代码输出什么?A:1;B:2;C:panic;D:不能编译。 package main import “fmt“ func main() { m := m

    2022-10-12:以下go语言代码输出什么?A:1:B:2:C:panic:D:不能编译. package main import "fmt" func main() { m ...

  4. 2022-05-03:Alice 和 Bob 再次设计了一款新的石子游戏。现有一行 n 个石子,每个石子都有一个关联的数字表示它的价值。给你一个整数数组 stones ,其中 stones[i] 是第

    2022-05-03:Alice 和 Bob 再次设计了一款新的石子游戏.现有一行 n 个石子,每个石子都有一个关联的数字表示它的价值.给你一个整数数组 stones ,其中 stones[i] 是第 ...

  5. 2022-02-15:扫地机器人。 房间(用格栅表示)中有一个扫地机器人。 格栅中的每一个格子有空和障碍物两种可能。 扫地机器人提供4个API,可以向前进,向左转或者向右转。每次转弯90度。 当扫地机

    2022-02-15:扫地机器人. 房间(用格栅表示)中有一个扫地机器人. 格栅中的每一个格子有空和障碍物两种可能. 扫地机器人提供4个API,可以向前进,向左转或者向右转.每次转弯90度. 当扫地机 ...

  6. vue全家桶进阶之路43:Vue3 Element Plus el-form表单组件

    在 Element Plus 中,el-form 是一个表单组件,用于创建表单以便用户填写和提交数据.它提供了许多内置的验证规则和验证方法,使表单验证更加容易. 使用 el-form 组件,您可以将表 ...

  7. pycharm eslint 关闭

    pycharm 关闭eslint 文件->设置->语言和框架->JavaScript->代码质量工具->ESLint

  8. 【GiraKoo】安装Visual Assist失败,提示“此扩展已经安装到所有适用的产品”

    [问题解决]安装Visual Assist失败,提示"此扩展已经安装到所有适用的产品" 在安装Visual Assist插件时,提示错误. 点击下一步之后,进入插件安装界面.插件安 ...

  9. 什么是 Spring?为什么学它?

    前言 欢迎来到本篇文章!在这里,我将带领大家快速学习 Spring 的基本概念,并解答两个关键问题:什么是 Spring,以及为什么学习 Spring. 废话少说,下面,我们开始吧! Spring 官 ...

  10. 代码随想录算法训练营Day16二叉树|104.二叉树的最大深度 559.n叉树的最大深度 111.二叉树的最小深度  222.完全二叉树的节点个数

    代码随想录算法训练营 代码随想录算法训练营Day16二叉树|104.二叉树的最大深度 559.n叉树的最大深度 111.二叉树的最小深度  222.完全二叉树的节点个数 104.二叉树的最大深度 题目 ...