给你一个链表的头节点 $head$ 和一个整数 $val$ ,请你删除链表中所有满足 $Node.val == val$ 的节点,并返回 新的头节点

示例 1:


1
2
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

1
2
输入:head = [], val = 1
输出:[]

示例 3:

1
2
输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 $[0, 10^{4}]$ 内
  • $1 <= Node.val <= 50$
  • $0 <= val <= 50$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode p = new ListNode(-1, head);
ListNode p0 = p;
while(p.next!=null){
if(p.next.val==val){
p.next=p.next.next;
}
else{
p=p.next;
}
}

return p0.next;
}
}

在这里产生了疑惑,对于下面两行代码:

1
2
ListNode p = new ListNode(-1, head);
ListNode p0 = p;

p在后续的操作中会改变,但是p0不会改变,这是为什么呢?两者是同一对象的引用啊。通过询问chatgpt4,得到了答案:

1
2
3
4
5
6
7
这是因为在Java中,引用变量(比如 p 和 p0)实际上存储的是对象在内存中的地址。当我们说 "p 和 p0 是同一个对象的引用" 时,我们的意思是它们存储的地址是一样的,也就是说它们都指向内存中的同一个位置。

然而,当我们在代码中写 p = p.next; 时,我们实际上改变的是 p 存储的地址,而不是这个地址指向的对象本身。这行代码的意思是 "让 p 存储 p.next 当前存储的地址"。这就导致了 p 现在指向了链表中的下一个节点,而 p0 还是指向原来的节点。

因此,p 的改变并不会影响 p0。因为 p 和 p0 是独立的变量,当我们改变 p 存储的地址时,p0 存储的地址并不会改变。这就是为什么在 while 循环后,p 和 p0 的值会不同。

这是Java语言中对引用的处理方式,也是很多其他面向对象语言的通用规则。希望这个解释能帮助你理解。

2024.2.5

使用c++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummy = new ListNode();
dummy->next=head;
ListNode* cur = head;
ListNode* t = dummy;
while(cur!=NULL){
if(cur->val==val){
t->next=cur->next;
}
else{
t->next=cur;
t=t->next;
}
cur=cur->next;
}
return dummy->next;
}
};

这种方法使用了三个指针,dummy负责建立一个空头节点,cur负责遍历原链表,t负责随着cur的遍历而移动以建立新链表。这种方法是自然而然想到的。

还有一种方法,使用两个指针就可以,把t优化掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummy = new ListNode();
dummy->next=head;
ListNode* cur=dummy;
while(cur->next!=NULL){
if(cur->next->val==val){
cur->next=cur->next->next;
}
else{
cur=cur->next;
}
}
return dummy->next;
}
};

为什么使用cur->next而不是cur?

我的理解是使用cur->next可以使cur节点在遍历的一开始可以有选择地回避一些不符合要求地节点,cur的作用是在遍历的同时穿起一个符合要求的链表。而dummy起到一个记录起点的作用。也是因为无法无脑落地的原因,所以使用它的dummy.next