当前位置: 首页 > news >正文

Leetcode 02 java

160. 相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。

/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {}
}

1 为什么?我以为java没有指针的?

在 Java 里,虽然没有像 C、C++ 那样可以直接操作内存地址的指针,却有引用(Reference)的概念。

这两个概念在功能上是类似的,但在使用安全性和语法上存在差异。下面为你详细解释:

1. 引用与指针的差异

对比项指针(C/C++)引用(Java)
直接操作内存能够直接访问和修改内存地址无法直接访问内存地址,操作的是对象的引用
空值处理可以赋值为NULL(C)或nullptr(C++)可以赋值为null
算术运算支持指针算术运算(如p++不支持引用算术运算
安全性容易引发内存泄漏和野指针问题由垃圾回收机制管理内存,减少了内存错误

2. Java 中的引用

在 Java 里,当你创建一个对象时,实际上得到的是指向这个对象的引用,并非对象本身。比如:

java

ListNode node = new ListNode(1); // node是引用,指向堆内存中的ListNode对象

3. 链表操作中的引用

在处理链表时,Java 使用引用来实现节点之间的连接。就像这样:

java

public class ListNode {int val;ListNode next; // next是引用,指向下一个ListNode对象ListNode(int x) { val = x; }
}

在之前的getIntersectionNode方法中:

  • currA = currA.next;的作用是将当前引用移动到下一个节点。
  • if (currA == currB)是在比较两个引用是否指向同一个对象,而不是比较对象的值。

4. 为何 Java 使用引用而非指针

  • 安全性:引用降低了内存错误出现的概率,像空指针异常在编译时就能被发现。
  • 简化编程:无需手动管理内存,垃圾回收机制会自动处理不再使用的对象。
  • 跨平台性:引用让 Java 更符合 "一次编写,到处运行" 的特性。

5. 示例对比

下面通过代码对比 Java 引用和 C++ 指针在链表操作上的差异:

Java(引用)

java

ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
node1.next = node2; // node1的next引用指向node2

C++(指针)

cpp

运行

struct ListNode {int val;ListNode* next; // 指针成员ListNode(int x) : val(x), next(nullptr) {}
};ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
node1->next = node2; // 通过指针访问成员并赋值

总结

Java 虽然没有显式的指针,但引用的功能和指针类似,都是用来指向对象。在链表操作中,currA = currA.next这样的代码其实就是在移动引用,和 C++ 中移动指针的逻辑是一样的,只是语法和安全性有所不同。

2 next方法是自己定义的还是调用的吗?(其实我想表达的是next是内置方法还是调用的成员变量)

在 Java 的链表操作里,next并非方法,而是ListNode类中定义的一个成员变量,其类型为ListNode它的作用是指向下一个节点,以此实现链表中节点的连接。下面为你详细解析:

1. next是成员变量,不是方法

在链表节点类ListNode的定义中,一般会包含一个val(节点值)和next(指向下一个节点的引用),类似这样:

java

public class ListNode {int val;         // 节点存储的值ListNode next;   // 指向下一个节点的引用ListNode(int x) { val = x; }  // 构造函数
}

所以,next是一个公共的成员变量,而非方法。访问它的方式是node.next,而不是node.next()

2. 在链表操作中使用next

在遍历链表或者操作链表时,我们会直接使用next来获取下一个节点。例如:

java

ListNode current = head;  // 从链表头开始
while (current != null) {System.out.println(current.val);  // 输出当前节点的值current = current.next;  // 移动到下一个节点
}

这里的current.next就是直接访问ListNode类的next成员变量。

3. 为什么会误解为方法?

有些编程语言(如 Python 的链表实现)可能会使用next()方法来获取下一个节点。另外,如果ListNode类被封装得更完善,next可能会被设计成私有变量,同时提供公有的getNext()方法来访问它,就像这样:

java

public class ListNode {private int val;private ListNode next;  // next被设为私有public ListNode getNext() {  // 提供公有的访问方法return next;}// 其他方法...
}

不过,在常见的算法题(如 LeetCode)中,ListNode类通常会被简化,next会直接作为公有变量暴露出来,这样可以减少代码的冗余。

4. 总结

  • next是成员变量:在标准的链表实现中,nextListNode类的公有成员变量,通过node.next来访问。
  • 不是方法:除非链表类被特别设计成提供getNext()方法,否则不需要加括号调用。
  • 代码示例:在之前的getIntersectionNode方法中,currA = currA.next就是在直接访问next变量,将当前节点的引用移动到下一个节点。

3 点调用方法and链表

你提到的 “点调用方法” 确实是 Java 中调用对象方法的语法,但在链表操作里,next的性质需要根据具体的类定义来确定。下面详细说明:

1. next是变量还是方法,取决于类的设计

在最常见的链表节点定义中,next是一个公共变量,而非方法。例如:

java

public class ListNode {int val;ListNode next;  // 公共变量,指向下一个节点ListNode(int x) { val = x; }
}

此时,使用点号直接访问变量

java

ListNode current = head;
current = current.next;  // 直接访问next变量

2. 封装设计:将next设为私有变量,并提供方法访问

如果类设计更注重封装性,next可能被设为私有变量,同时提供公共方法来访问:

java

public class ListNode {private int val;private ListNode next;  // 私有变量public ListNode getNext() {  // 公共方法return next;}public void setNext(ListNode node) {this.next = node;}
}

此时,需要通过方法调用来访问:

java

ListNode current = head;
current = current.getNext();  // 通过方法调用

3. 实际应用中的差异

  • 算法题(如 LeetCode):为了简化代码,通常直接使用公共变量next
  • 工程实践:更倾向于封装,使用getNext()方法。

4. 如何判断?看类定义!~~~~~~~~~~~~~~~~~~~~~~~~~~~

如果你看到的代码是这样:

java

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {ListNode currA = headA;while (currA != null) {currA = currA.next;  // 这里next是变量}// ...
}

可以推断,ListNode类的定义中next是一个公共变量(否则会编译错误)。

5. 总结

场景访问方式代码示例
next是公共变量直接访问变量currA = currA.next
next是私有变量,通过方法暴露调用方法currA = currA.getNext()

在 Java 中,点号(.)既可以访问变量,也可以调用方法,具体取决于类的设计。在链表操作中,两种方式都可能存在,但算法题中更常见的是直接访问公共变量next

4 双指针算法的思路

pA走过的路径为A链+B链

pB走过的路径为B链+A链

pA和pB走过的长度都相同,都是A链和B链的长度之和,相当于将两条链从尾端对齐,如果相交,则会提前在相交点相遇,如果没有相交点,则会在最后相遇。

5 解

/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {// 先进行边界条件判断,要是有一个链表为空,那肯定没有相交节点,直接返回 nullif (headA == null || headB == null)return null;// 初始化两个指针 pA 和 pB,分别指向两个链表的头节点ListNode pA = headA, pB = headB;// 开始循环,当 pA 和 pB 指向同一个节点时停止循环while (pA != pB) {// 要是 pA 指针走到了链表 A 的末尾(也就是 null),就把它重新指向链表 B 的头节点// 否则就移动到下一个节点pA = pA == null ? headB : pA.next;// 要是 pB 指针走到了链表 B 的末尾(也就是 null),就把它重新指向链表 A 的头节点// 否则就移动到下一个节点pB = pB == null ? headA : pB.next;}// 当循环结束后,pA 和 pB 要么指向相交节点,要么都为 null(说明两个链表不相交)// 不管怎样,直接返回 pA 就行return pA;}
}

代码核心思路剖析

这个算法运用了双指针法,其巧妙之处在于让两个指针在遍历链表时 “交换路径”。具体来说:

  1. 指针 pA 按顺序遍历链表 A,当遍历完链表 A 后,接着从链表 B 的头节点开始继续遍历。
  2. 指针 pB 按顺序遍历链表 B,当遍历完链表 B 后,接着从链表 A 的头节点开始继续遍历。

要是两个链表存在相交节点,那么这两个指针最终一定会在相交节点相遇。这是因为当两个指针都走过len(A) + len(B)的距离时,它们必然会同时到达相交节点或者链表末尾。

如果两个链表不相交,那么两个指针会同时到达各自链表的末尾(也就是 null),此时循环结束,返回 null。

算法特点

  • 时间复杂度为 O (m + n),这里的 m 和 n 分别是两个链表的长度。
  • 空间复杂度为 O (1),只需要使用常数级的额外空间。

6 ListNode pA = headA, pB = headB;//自己定义了一个listnode类,其实相当于int a 然后赋值吧?

是的,你可以把它类比成基本数据类型的变量赋值,但这里有一些细节需要留意。

变量赋值本质

在 Java 里,ListNode pA = headA这样的语句,本质上是在进行引用赋值,而非值拷贝。下面为你详细解释:

  • 基本数据类型(像int a = 10):这种情况下,变量a会直接存储数值10
  • 引用数据类型(例如ListNode pA = headA):此时变量pA存储的是对象的引用,并非对象本身。打个比方,如果headA指向链表的头节点,那么pA就会指向同一个节点,它们两个引用的是同一块内存区域。

代码示例类比

下面通过一个例子,帮你更好地理解引用赋值和值拷贝的区别:

java

// 基本数据类型:值拷贝
int a = 10;
int b = a;  // b 获得了 a 的值 10
a = 20;     // 修改 a 的值,b 的值不会受到影响
System.out.println(b);  // 输出:10// 引用数据类型:引用赋值
ListNode node1 = new ListNode(1);
ListNode node2 = node1;  // node2 和 node1 引用同一个节点
node1.val = 2;           // 修改 node1 的值,node2 的值也会改变
System.out.println(node2.val);  // 输出:2

链表遍历的应用

在链表遍历的场景中,我们经常会复制头节点的引用,这样做是为了在遍历过程中不会丢失链表的头部。就像这样:

java

ListNode pA = headA;  // 复制头节点的引用
while (pA != null) {// 对当前节点 pA 进行操作pA = pA.next;     // 移动到下一个节点,headA 不会发生改变
}

双指针法的逻辑

在你提供的代码中,pApB这两个指针分别从两个链表的头节点开始遍历,并且会在遍历结束后切换到另一个链表继续遍历。它们的相遇条件是基于引用相等(也就是指向同一个节点),而不是值相等。

java

while (pA != pB) {  // 这里比较的是引用是否相等pA = pA == null ? headB : pA.next;pB = pB == null ? headA : pB.next;
}

总结

  • 从形式上看,ListNode pA = headAint a = 10类似,都是变量赋值语句。
  • 但从本质上来说,基本数据类型存储的是值,而引用数据类型存储的是对象的引用。
  • 在链表操作时,复制引用是很常见的操作,这样可以避免修改原始的头节点,同时能够遍历链表。

7 引用相等和值相等

在 Java 中,引用相等和值相等是两个不同的概念,它们的比较方式和用途各不相同。以下是详细的解释:

1. 引用相等(Reference Equality)

  • 比较对象:比较两个引用变量是否指向同一个对象实例(即是否指向相同的内存地址)。
  • 比较运算符:使用 == 进行比较。
  • 示例

    java

    ListNode a = new ListNode(1);
    ListNode b = a;          // b 和 a 指向同一个对象
    ListNode c = new ListNode(1);  // c 指向另一个值相同的对象System.out.println(a == b);  // 输出:true(引用相同)
    System.out.println(a == c);  // 输出:false(引用不同,内存地址不同)
    
  • 在链表中的应用
    在你的代码中,pA != pB 比较的是两个指针是否指向同一个节点(内存地址相同),而非节点的值是否相等。

2. 值相等(Value Equality)

  • 比较对象:比较两个对象的内容是否相同(例如,对象的字段值是否全部相等)。
  • 比较方法:通常通过重写 equals() 方法实现。
  • 示例

    java

    ListNode a = new ListNode(1);
    ListNode b = new ListNode(1);  // b 和 a 值相同,但不是同一个对象// 如果 ListNode 类重写了 equals() 方法:
    System.out.println(a.equals(b));  // 输出:true(值相同)
    System.out.println(a == b);       // 输出:false(引用不同)
    
  • 默认 equals() 方法的行为
    如果类没有重写 equals() 方法(例如 Java 中的 Object 类),则 equals() 等价于 ==,比较的仍然是引用。因此,自定义类通常需要重写 equals() 方法来比较值。

3. 链表节点的比较

在你的代码中:

java

while (pA != pB) { ... }
  • pA 和 pB 是引用变量,pA != pB 比较的是它们是否指向同一个节点对象内存地址是否相同)。
  • 如果两个链表相交,那么它们在相交点之后的所有节点都是同一个对象(共享内存地址),因此 pA 和 pB 最终会指向该节点,循环终止。
  • 如果不相交,pA 和 pB 会同时变为 null(引用相同),循环也会终止。

总结

比较类型运算符 / 方法比较内容示例
引用相等==两个引用是否指向同一内存地址a == b
值相等equals()两个对象的内容是否相同a.equals(b)

关键点

  • == 比较的是内存地址(引用相等)。
  • equals() 比较的是值(需要类重写该方法)。

8 我现在的印象,java里面好像就是很多都是比较内存地址的,比较值的用equals的,要重写比较容易看出来吧?

是的,你的理解很准确!在 Java 中,默认的比较方式是基于引用(内存地址),而值比较通常需要通过重写 equals() 方法来实现,基本数据类型比较的不是内存地址,而是直接比较它们的值

以下是更具体的总结和注意事项:

1. Java 中的默认比较行为

  • 基本数据类型(如 intcharboolean 等):
    使用 == 直接比较值。

    java

    int a = 10;
    int b = 10;
    System.out.println(a == b);  // 输出:true(值相等)
    
  • 引用数据类型(如 StringList, 自定义类等):
    使用 == 比较引用(内存地址),而非值内容。

    java

    String s1 = new String("hello");
    String s2 = new String("hello");
    System.out.println(s1 == s2);  // 输出:false(引用不同)
    

2. 值比较需要重写 equals() 方法

  • Java 内置类(如 StringIntegerArrayList 等):
    这些类已经重写了 equals() 方法,直接使用即可比较值。

    java

    String s1 = new String("hello");
    String s2 = new String("hello");
    System.out.println(s1.equals(s2));  // 输出:true(值相等)
    
  • 自定义类(如 ListNode):
    如果需要比较对象的值,必须显式重写 equals() 方法。否则,equals() 默认等价于 ==(比较引用)。

    java

    public class ListNode {int val;ListNode next;// 重写 equals() 方法@Overridepublic boolean equals(Object o) {if (this == o) return true;  // 引用相同直接返回 trueif (o == null || getClass() != o.getClass()) return false;ListNode node = (ListNode) o;return val == node.val && next == node.next;  // 比较值和 next 引用}
    }
    

3. 如何快速判断是否需要重写 equals()

  • 看类是否为自己定义的
    如果是自定义类(如 ListNodePersonStudent 等),默认没有重写 equals(),需要自己实现。

  • 看类是否是 Java 内置类
    常见的内置类(如 StringIntegerList 等)通常已经重写了 equals(),直接使用即可。

  • IDE 工具提示
    在 IDE 中(如 IntelliJ IDEA、Eclipse),当你使用 equals() 比较自定义类时,IDE 可能会提示 “equals() 方法未被重写”。

4. 常见误区❌!!!!!!!

  • 字符串比较用 ==

    java

    String s1 = "hello";
    String s2 = "hello";
    System.out.println(s1 == s2);  // 可能输出 true(字符串常量池优化)String s3 = new String("hello");
    System.out.println(s1 == s3);  // 输出 false(new 创建的新对象)
    
     

    正确做法:始终使用 equals() 比较字符串内容。

  • 包装类(如 Integer)的比较

    java

    Integer a = 100;
    Integer b = 100;
    System.out.println(a == b);  // 输出 true(-128~127 之间的整数缓存)Integer c = 200;
    Integer d = 200;
    System.out.println(c == d);  // 输出 false(超出缓存范围,创建新对象)
    
     

    正确做法:包装类比较值也用 equals()

总结

场景比较方式示例
基本数据类型==(比较值)int a = 10; a == 10
引用类型(默认)==(比较引用)ListNode a = new ListNode(1);
引用类型(值比较)equals()a.equals(b)(需重写方法)

记忆口诀

  • == 比地址,equals() 比值(但 equals() 需要类重写才能生效)。
  • 自定义类必须重写 equals() 才能比较值

在 Java 中,基本数据类型比较的不是内存地址,而是直接比较它们的值。这是基本数据类型和引用数据类型在比较行为上的核心区别。

为什么基本数据类型不比较内存地址?

基本数据类型(如 intcharboolean 等)是 “值类型”,它们在内存中直接存储的是具体的数值,而不是像引用类型那样存储 “对象的内存地址”。

例如:

java

int a = 10;
int b = 10;
  • 变量 a 和 b 直接存储的是 10 这个数值(存储在栈内存中)。
  • 当用 a == b 比较时,直接判断两个数值是否相等(这里都是 10),所以结果为 true

和引用类型的对比

引用类型(如 String、自定义类 ListNode 等)存储的是 “对象在堆内存中的地址”,而不是对象本身的值。

例如:

java

ListNode node1 = new ListNode(5);
ListNode node2 = new ListNode(5);
  • node1 和 node2 存储的是两个不同对象的内存地址(堆中两个独立的 ListNode 对象)。
  • 用 node1 == node2 比较时,实际是比较这两个地址是否相同(显然不同),所以结果为 false

总结

数据类型== 比较的内容举例
基本数据类型直接比较值(不是地址)int a=10; int b=10; a==b → true
引用数据类型比较内存地址(引用)ListNode n1, n2; n1==n2 → 比较地址

简单说:基本类型存值,比较值;引用类型存地址,比较地址

http://www.dtcms.com/a/283166.html

相关文章:

  • IDEA报错“资源找不到”?重启就好了!!?
  • 使用Dify构建HR智能助理,深度集成大模型应用,赋能HR招聘管理全流程,dify相关工作流全开源。
  • 城市蓝影.
  • 服务注册nacos和OpenFerign(用于封装跨服务之间的调用方法)
  • kubernetes学习笔记(一)
  • 数据结构 双向链表(2)--双向链表的实现
  • 黄仁勋链博会演讲实录:脱掉皮衣,穿上唐装,中文开场
  • 完善评论发布功能
  • PHP面向对象编程:类与对象的基础概念与实践
  • 从0到1搭建Lazada账号矩阵:自养号测评的精细化养号全攻略
  • Linux 定时器应用示例
  • 功能测试和回归测试
  • C# WPF后台设置控件样式失效的解决方法
  • 【Vue】tailwindcss + ant-design-vue + vue-cropper 图片裁剪功能(解决遇到的坑)
  • 从规模到效率:大模型三大定律与Chinchilla定律详解
  • 实现通讯录人员选择
  • IKE学习笔记
  • Java强化:多线程及线程池
  • 从电子管到CPU
  • 基于MATLAB的决策树DT的数据分类预测方法应用
  • Android CameraX使用
  • [析]Deep reinforcement learning for drone navigation using sensor data
  • CClink IEF Basic设备数据 保存到MySQL数据库项目案例
  • 高德地图MCP服务使用案例
  • 解锁数据交换的魔法工具——Protocol Buffers
  • 矿业自动化破壁者:EtherCAT转PROFIBUS DP网关的井下实战
  • ABP VNext + EF Core 二级缓存:提升查询性能
  • Mysql系列--1、库的相关操作
  • Mybatis-2快速入门
  • @Binds/@IntoMap/@ClassKey的使用