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
是成员变量:在标准的链表实现中,next
是ListNode
类的公有成员变量,通过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;}
}
代码核心思路剖析
这个算法运用了双指针法,其巧妙之处在于让两个指针在遍历链表时 “交换路径”。具体来说:
- 指针 pA 按顺序遍历链表 A,当遍历完链表 A 后,接着从链表 B 的头节点开始继续遍历。
- 指针 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 不会发生改变
}
双指针法的逻辑
在你提供的代码中,pA
和pB
这两个指针分别从两个链表的头节点开始遍历,并且会在遍历结束后切换到另一个链表继续遍历。它们的相遇条件是基于引用相等(也就是指向同一个节点),而不是值相等。
java
while (pA != pB) { // 这里比较的是引用是否相等pA = pA == null ? headB : pA.next;pB = pB == null ? headA : pB.next;
}
总结
- 从形式上看,
ListNode pA = headA
和int 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 中的默认比较行为
-
基本数据类型(如
int
,char
,boolean
等):
使用==
直接比较值。java
int a = 10; int b = 10; System.out.println(a == b); // 输出:true(值相等)
-
引用数据类型(如
String
,List
, 自定义类等):
使用==
比较引用(内存地址),而非值内容。java
String s1 = new String("hello"); String s2 = new String("hello"); System.out.println(s1 == s2); // 输出:false(引用不同)
2. 值比较需要重写 equals()
方法
-
Java 内置类(如
String
,Integer
,ArrayList
等):
这些类已经重写了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()
?
-
看类是否为自己定义的:
如果是自定义类(如ListNode
,Person
,Student
等),默认没有重写equals()
,需要自己实现。 -
看类是否是 Java 内置类:
常见的内置类(如String
,Integer
,List
等)通常已经重写了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 中,基本数据类型比较的不是内存地址,而是直接比较它们的值。这是基本数据类型和引用数据类型在比较行为上的核心区别。
为什么基本数据类型不比较内存地址?
基本数据类型(如 int
、char
、boolean
等)是 “值类型”,它们在内存中直接存储的是具体的数值,而不是像引用类型那样存储 “对象的内存地址”。
例如:
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 → 比较地址 |
简单说:基本类型存值,比较值;引用类型存地址,比较地址。