链表的删除
一、最初的思路
设置了慢节点p1和快节点p2,遍历链表时p1紧跟p2,如果p2->val = val,那么通过 p1->next = p2->next; 把p2节点从节点中删除。
1、链表为空
直接返回head
if(head == nullptr) return head;
2、删除的节点为头节点
头节点需要指向头结点的next
head = head->next;
注:这里可能会考虑到单节点的情况,[head]->null,如果head->val=val,那就要删除head,剩下的是null,head = head->next;是可以包括这种情况的
3、删除的节点不是头节点
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//1、删除节点为空
if(head == nullptr) return head;
ListNode* p1 = nullptr;
ListNode* p2 = head;
ListNode* temp = nullptr;
while(p2 != nullptr) {
if(p2->val == val) {
//2、删除的是头节点
if(p1 == nullptr) {
head = head->next;
p2 = head;
/*因为要删除链表中《所有》值为val的节点,
所以要遍历完整个链表
这里的p2=head保证了后续链表的遍历*/
}
else { //3、删除的不是头节点
p1->next = p2->next;//把p2节点从链表删除
p2 = p1->next;
/*p2 = p1->next;保证遍历完整个链表*/
}
}
else {
temp = p2;
p2 = p2->next;
p1 = temp;
}
}
return head;
}
};
运行成功。
二、看了题解后的优化
看了题解发现可以简化写法。核心思想是增加一个哑节点。这样情况1(链表为空)情况2(删除的节点为头节点)都可以合并进 情况3中(删除的节点不是头节点)。
需要注意的点
(1)链表中节点的构造
力扣官方对单链表的定义:
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) {}
};
由此,创造一个节点可以写成:
struct ListNode* dummyHead = new ListNode(0, head);
(2)上文中的情况三“删除的节点不是头节点”,通过快慢指针删除对应的节点,可以通过一个指针temp完成
struct ListNode* temp;
temp->next = temp->next->next;
代码如下:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
struct ListNode* fhead = new ListNode(0,head);//1
struct ListNode* temp = fhead;
//2思考这里为什么要用temp->next 而不是temp
//尾节点 头节点
while(temp->next != NULL) {
if(temp->next->val == val) {
temp->next = temp->next->next;
}
else {//3
temp = temp->next;
}
}
return fhead->next;//4
}
};
犯了很多错误:
(1)哑节点的构造
题目的构造体没有用 typedef,不可以省略关键字 struct
struct ListNode* fhead = new ListNode(0,head);
//这样是错的,因为构造体并没有用typedef
ListNode* fhead = new ListNode(0,head);
(2)构造了哑节点 fhead 后的返回值
构造了哑节点 fhead 之后,注意返回值是 fhead->next,fhead 本身是不属于单链表的。
return fhead->next;
(3)temp->next 和 temp
为什么要比较 temp->next 节点不能用 temp 节点呢?
删除节点的关键步骤为 temp->next = temp->next->next; (判断的是 temp->next 这个节点)。本质上其实是用temp代替p1,temp->next代替 p2。
但如果判断变成temp这个节点(也就是假如判断的条件是if(temp->val = val;) ),要删除temp节点的话,是需要temp的上一个节点的,这里只用了temp遍历,是找不到上一个节点的。
为什么 while 语句中的判断条件是temp->next = NULL
遍历结束的条件是,temp->next = NULL;
(4)nullptr 和 NULL的区别。
接上一条,while(temp->next != NULL) 和 while(temp->next != nullptr) 的区别是什么?
NULL属于C 语言中的宏,后来 C++11 引入了 nullptr 关键字,都用来表示空指针。
在 C++ 中表示指针的地方,使用 nullptr 表示空指针。
https://blog.csdn.net/jiey0407/article/details/125383209?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169960180916800182157513%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=169960180916800182157513&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-125383209-null-null.142v96pc_search_result_base6&utm_term=nullptr%20%E5%92%8C%20NULL&spm=1018.2226.3001.4187
(5)while循环里面,如果写成如下形式 是错的:
while(temp->next != NULL) {
if(temp->next->val == val) {
temp->next = temp->next->next;
}
temp = temp->next;
}
会报错:
Line 19: Char 21: runtime error: member access within null pointer of type ‘struct ListNode’ (solution.cpp)
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:28:21
为什么一定需要那个else?
代码是通过判断 temp->next 这个节点的值是否为 val 进行删除操作的。
如图:假设删除了temp->next(a2),之后,a3会变成新的temp->next节点,所以要对a3进行判断。
因此在进行了删除操作 temp->next = temp->next->next; 之后,temp的位置是不能变化的。
如果不进行删除操作,代表此时的 temp->next->val != val ,就需要把 temp往后移 temp = temp->next;
因此 else是必须要的
三、释放内存
C++编写的代码都需要释放内存,也就是上述所有代码其实都不完全正确,但是在力扣中是可以通过的。一定要注意释放内存。
delete temp;
四、Java实现
改于2023.12.11,之前是用C++写的,最近在学Java,用Java重写了一次。
class Solution {
public ListNode removeElements(ListNode head, int val) {
//链表删除
//空链表
//非空链表 添加头节点
if(head == null) return null;
ListNode myheah = new ListNode(-1,head);
//这里需不需要一个临时变量呢?
ListNode myh = myheah;
while(myh.next != null) {//
if(myh.next.val == val){
myh.next = myh.next.next;
} else {
myh = myh.next;
}
}
return myheah.next;
}
}
分析:
注意这里是需要临时变量的。new ListNode(-1,head)
new在堆中开辟了一个空间,开辟空间的同时分配了一个地址,存在 myheah 引用中。如果遍历的时候用 myheah 这个变量,那while循环之后,链表的头节点地址是不清楚的。
四、递归解法
(还未完成)