目录
一、题目简介
二、解题思路
三、代码
3.1 直接使用原来的链表来进行移除节点操作
1. 时间复杂度
删除头节点:
删除非头节点:
综合分析:
2. 空间复杂度
总结:
3.2 设置虚拟头节点
时间复杂度:
空间复杂度:
结论:
链表操作中,可以使用原链表来直接进行删除操作,也可以设置一个虚拟头结点再进行删除操作,接下来看一看哪种方式更方便。
一、题目简介
题目链接:203. 移除链表元素 - 力扣(LeetCode)
题目概述:给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1 输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7 输出:[]提示:
- 列表中的节点数目在范围
[0, 10^4]
内 1 <= Node.val <= 50
0 <= val <= 50
二、解题思路
这里以链表 1 4 2 4 来举例,移除元素4。
如果使用C,C++编程语言的话,不要忘了还要从内存中删除这两个移除的节点, 清理节点内存之后如图:
当然如果使用java ,python的话就不用手动管理内存了。
还要说明一下,就算使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养成手动清理内存的习惯。
建议删除之后的节点要及时清理该节点所占用的内存,如果不及时清理,会引发内存泄漏问题:
内存泄漏的定义:内存泄漏(Memory Leak)是指在程序运行过程中,程序没有及时释放已经不再使用的内存资源,导致这些资源无法被操作系统回收,从而导致系统的可用内存逐渐减少,最终可能导致程序或系统崩溃。
这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了,那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表中的第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢?
这里就涉及如下链表操作的两种方式:
- 直接使用原来的链表来进行删除操作。
- 设置一个虚拟头结点在进行删除操作。
来看第一种操作:直接使用原来的链表来进行移除。
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。
依然别忘将原头结点从内存中删掉。
这样移除了一个头结点,是不是发现,在单链表中移除头结点 和 移除其他节点的操作方式是不一样,其实在写代码的时候也会发现,需要单独写一段逻辑来处理移除头结点的情况。
那么可不可以 以一种统一的逻辑来移除 链表的节点呢。
其实可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。
这里来给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1。
这样是不是就可以使用和移除链表其他节点的方式统一了呢?
来看一下,如何移除元素1 呢,还是熟悉的方式,然后从内存中删除元素1。
最后呢在题目中,return 头结点的时候,别忘了 return dummyNode->next;
, 这才是新的头结点。
三、代码
3.1 直接使用原来的链表来进行移除节点操作
#include<iostream>
// 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。struct ListNode
{int data; // 定义节点数据域ListNode* next; // 定义节点指针域ListNode(int x) :data(x),next(nullptr){} // 节点的构造函数
};ListNode* removeElements(ListNode* head, int val){// 删除头节点//这段代码的目标是删除链表中所有值为 val 的连续头节点。每次删除一个节点后,头指针 head 被更新为下一个节点,直到找到一个节点的值不等于 val。while(head!=nullptr && head->data==val){ // 注意这里不能是 ifListNode* tmp = head; // 创建一个指向当前头节点的指针head = head->next; // 更新head指针,指向当前节点的下一个节点,即将链表的头节点向后移动delete tmp; // 释放掉删掉的节点的内存,避免内存泄漏}// 删除非头节点ListNode* ptr1 = head; // 定义链表遍历指针while(ptr1->next!=nullptr && ptr1!=nullptr){ // 遍历整个链表if(ptr1->next->data==val){ListNode* tmp = ptr1->next;ptr1->next = ptr1->next->next;delete tmp; // 释放掉被删除节点所占用的内存,避免内存泄漏}else{ // 不写 else ,即在这里只写 ptr1=ptr1->next;,程序运行会面临崩溃ptr1=ptr1->next;} }return head;
}int main(){// 定义一个新链表 1--2--6--2ListNode* L_head = new ListNode(1); // 新链表的头节点为L_headL_head->next = new ListNode(2);L_head->next->next = new ListNode(6);L_head->next->next->next = new ListNode(2);int val=2; // 定义要删除的元素ListNode* new_head = removeElements(L_head,val);return 0;
}
对上述代码进行分析:
一、删除头节点需单独处理
// 删除头节点//这段代码的目标是删除链表中所有值为 val 的连续头节点。每次删除一个节点后,头指针 head 被更新为下一个节点,直到找到一个节点的值不等于 val。while(head!=nullptr && head->data==val){ // 注意这里不能是 ifListNode* tmp = head; // 创建一个指向当前头节点的指针head = head->next; // 更新head指针,指向当前节点的下一个节点,即将链表的头节点向后移动delete tmp; // 释放掉删掉的节点的内存,避免内存泄漏}
子代码第一行:必须使用while循环而不能使用if判断语句,原因如下:
假设有一个链表,元素为 1 -> 1 -> 1 -> 2 -> 3
,我们需要删除所有值为 1
的节点。如果我们使用 if
语句,那么当删除第一个值为 1
的节点后,程序会跳出 if
语句,导致后续值为 1
的节点无法被继续删除。因为 if
语句只会在条件满足时执行一次,删除一个节点后就停止了对后续节点的检查。
而使用 while
循环可以确保在删除当前节点后,循环会继续判断下一个节点是否符合条件,从而能够连续删除所有值为 1
的节点。通过 while
循环,直到没有更多的值为 1
的节点为止,才能确保所有目标节点都被删除。
二、删除非头节点的逻辑
正确形式的代码如下,记为A。
// 正确形式A
// 删除非头节点ListNode* ptr1 = head; // 定义链表遍历指针while(ptr1->next!=nullptr && ptr1!=nullptr){ // 遍历整个链表if(ptr1->next->data==val){ListNode* tmp = ptr1->next;ptr1->next = ptr1->next->next;delete tmp; // 释放掉被删除节点所占用的内存,避免内存泄漏}else{ // 不写 else ,即在这里只写 ptr1=ptr1->next;,程序运行会面临崩溃ptr1=ptr1->next;} }
为什么while循环判断条件是 (ptr1->next!=nullptr && ptr1!=nullptr),单向链表在删除节点时,必须要找到被删除节点的前一个节点,因此我们用ptr1记录被删除节点的前一个节点,而ptr1->next表示被删除的节点。
上述代码中else必须存在,不能写成如下形式,错误形式的代码记为B:
// 错误形式B
// 删除非头节点ListNode* ptr1 = head; // 定义链表遍历指针while(ptr1->next!=nullptr && ptr1!=nullptr){ // 遍历整个链表if(ptr1->next->data==val){ListNode* tmp = ptr1->next;ptr1->next = ptr1->next->next;delete tmp; // 释放掉被删除节点所占用的内存,避免内存泄漏}// 不写 else ,即在这里只写 ptr1=ptr1->next;,程序运行会面临崩溃ptr1=ptr1->next;}
这两种形式的代码有什么区别:
A形式代码中,如果if判断条件不符合,那么程序会执行else分支,即会执行ptr1=ptr1->next;;如果if判断条件符合,那么程序便不会执行else分支,即不会执行ptr1=ptr1->next;。
B形式代码中,无论if判断条件符合或不符合,都会执行 ptr1=ptr1->next; 语句。
这种方法的时间复杂度和空间复杂度如下所示:
让我们分析这段代码的 时间复杂度 和 空间复杂度。
1. 时间复杂度
删除头节点:
while (head != nullptr && head->data == val)
这段代码的目标是删除所有满足条件(Node.val == val
)的头节点。它会循环遍历链表,直到找到一个不等于val
的节点或者链表为空。- 在最坏的情况下,如果链表中的所有节点的值都等于
val
,那么这个while
循环会遍历整个链表。假设链表有n
个节点,这个循环的时间复杂度是 O(n)。
删除非头节点:
while (ptr1 != nullptr && ptr1->next != nullptr)
这段代码用于遍历链表,删除值为val
的节点(但不包括头节点)。- 在最坏的情况下,这个循环会遍历整个链表一次,假设链表有
n
个节点,所以时间复杂度为 O(n)。 - 对于每个节点,删除操作本身的时间复杂度是 O(1)(因为只是更新指针并释放内存)。
综合分析:
- 在最坏的情况下,首先会有一次对整个链表的遍历(删除头节点),然后再进行一次遍历来删除非头节点。因此,整体时间复杂度是 O(n)。
2. 空间复杂度
- 在代码中,我们只使用了常数空间来存储指针
head
、ptr1
和tmp
。这些都是指向链表节点的指针,它们并不随着链表的大小变化而增加。 - 其他变量(如
val
)只是一个简单的整数,也不影响空间复杂度。 - 我们并没有创建额外的存储结构,如数组、哈希表等来存储节点,因此空间复杂度是 O(1)。
总结:
这样,代码的性能较为高效,适用于较长的链表。
3.2 设置虚拟头节点
#include<iostream>
// 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。struct ListNode
{int data; // 定义节点数据域ListNode* next; // 定义节点指针域ListNode(int x) :data(x),next(nullptr){} // 节点的构造函数
};// 设置一个虚拟头结点在进行移除节点操作(不需要单独考虑头节点的删除工作)
ListNode* removeElements_virtualHead(ListNode* head, int val){ListNode* virtualHead = new ListNode(0); // 创建一个虚拟头节点virtualHead->next = head; // 将虚拟头节点指向head// 接下来就是常规的删除非头节点操作ListNode* ptr = virtualHead; // 定义一个遍历指针while(ptr!=nullptr && ptr->next!=nullptr){if(ptr->next->data == val){ListNode* tmp = ptr->next;ptr->next = ptr->next->next;delete tmp;}else{ptr = ptr->next;}}head = virtualHead->next;delete virtualHead;return head;
}int main(){// 定义一个新链表 1--2--6--2ListNode* L_head = new ListNode(1); // 新链表的头节点为L_headL_head->next = new ListNode(2);L_head->next->next = new ListNode(6);L_head->next->next->next = new ListNode(2);int val=2; // 定义要删除的元素ListNode* new_head = removeElements_virtualHead(L_head,val);return 0;
}
分析这段代码的 时间复杂度 和 空间复杂度:
时间复杂度:
空间复杂度:
-
额外空间:
- 我们在代码中创建了一个虚拟头节点
virtualHead
,这是一个新的节点,所以占用了常数空间:O(1)
。 - 我们还使用了
ptr
和tmp
两个指针变量,都是常数空间的使用。
- 我们在代码中创建了一个虚拟头节点
-
链表本身的空间:
- 链表的节点是在原地修改的,没有额外创建新的节点,除了虚拟头节点外,其他节点不需要额外的存储空间。
-
综合分析:
- 代码的空间复杂度主要由
virtualHead
和局部指针ptr
、tmp
组成,这些都占用常数空间。 - 因此,空间复杂度是 O(1)。
- 代码的空间复杂度主要由
结论:
- 时间复杂度:
O(n)
,其中n
是链表的长度。 - 空间复杂度:
O(1)
,因为只有常数空间的额外开销。
推荐观看视频: