今天的题是移除链表元素,难度★★,主要难点是一下子想不到方案处理所有情况。通过今天的练习,我更加深刻地理解了++递归和迭代++的思想。
以后会将收获写在前面,便于自己复习。
递归: "自己调用自己" ------ 一个问题分解成和原问题逻辑相同但规模更小的子问题,直到子问题小到能直接解决(基线条件),再从最底层的结果逐层回推,最终解决原问题。
**迭代:**重复执行一组步骤,每一次执行都基于上一次的结果做改进,逐步逼近目标、完善结果或解决问题的过程,简单说就是 "步步优化的循环"
递归 VS 迭代
1.复杂度:时间复杂度一直;空间复杂度有差异:递归的空间复杂度为O(n),因为递归会占用调用栈空间,深度=链表长度,而迭代的空间复杂度为O(1)。
2.代码可读性:递归实现的代码逻辑简洁,贴合链表结构,代码可读性高;而迭代实现需要手动管理指针,边界多,代码可读性低
链表与递归
++链表的绝大多数核心操作都能通过递归实现,因为链表本身就是"递归定义"的数据结构++ ,每个结点包含数据和指向下一个结点的指针,而下一个结点又是一个链表,天然适配递归的分解子问题思路。链表使用递归的关键 是 ++先处理子链表,再处理当前节点++ ,终止条件是++子链表为空++
题目
203. 移除链表元素 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
我的思路
遍历链表各个结点,如果结点的值等于val,就将该结点删去。
昨天说明了++哑结点何时使用------头结点可能被删 / 改、空链表拼接、多链表合并++的时候必须使用哑结点辅助,而这道题存在头结点被删掉的可能,所以需要使用哑结点。
需要思考:如何遍历完整个链表,将所有等于val的结点删除
代码1
此处,我将head看作前驱,判断head->next->val是否等于val,如果等于,删除head->next即可,即使得head->next=head->next->next,因为需要使用head->next->val,所以我将循环条件设为head->next不为空。
需要注意的是:因为我将head看作前驱,循环内无法判断head->val是否等于val,所以在循环之前就要先判断head->val是否等于val,是否需要删除,需要删除的话,将dummy初始化为head->next即可。
cpp
/**
* 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) {
//判空
if(head==nullptr){
return head;
}
//头结点可以会被删除,所以需要使用哑结点
ListNode* dummy=new ListNode(-1);
dummy->next=head;
//如果头结点的值为val,删去头结点
if(head->val==val){
dummy->next=head->next;
}
while(head->next != nullptr){
//删除结点,需要使用前驱,所以这里判断的是后继的值
if(head->next->val==val){
head->next=head->next->next;
}
head=head->next;
}
ListNode* res=dummy->next;
delete dummy;
return res;
}
};
错误
在while(head->next!=nullptr)语句中出现错误:

因为在while循环中,最后的语句是head=head->next,++即使当head->next已经为nullptr时,仍会使head=head->next++,所以就出现了尝试查看空指针的next的错误
如图所示

代码2
基于代码1的错误进行修正:将while循环条件改为 head!=nullptr
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//判空
if(head==nullptr){
return head;
}
//头结点可以会被删除,所以需要使用哑结点
ListNode* dummy=new ListNode(-1);
dummy->next=head;
//如果头结点的值为val,删去头结点
if(head->val==val){
dummy->next=head->next;
}
while(head != nullptr){
//删除结点,需要使用前驱,所以这里判断的是后继的值
if(head->next->val==val){
head->next=head->next->next;
}
head=head->next;
}
ListNode* res=dummy->next;
delete dummy;
return res;
}
};
错误
上诉代码只处理了原链表头结点值为val的情况,但是无法处理链表结点全为val的情况

代码3
在创建哑结点之前,先利用循环找到第一个值不为val的结点,再进行后续处理。
对于链表的全部结点的值都为val的情况,head最后为空,而后续操作需要使用head->next,所以head为空时不能进行后续操作,直接返回空即可。
在第二个while循环中,if语句中++使用了head->next->val,所以需要限定head->next不为空++
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//判空
if (head == nullptr) {
return head;
}
//如果头结点为val,往后寻找到第一个不为val的结点
while (head != nullptr && head->val == val) {
head = head->next;
}
//如果不存在不为val的结点,直接返回null
if (head == nullptr) {
return head;
}
//存在不为val的结点,才继续下面的操作
//使用哑结点
ListNode* dummy = new ListNode(-1);
dummy->next = head;
while (head != nullptr) {
//head->next不为空,且值为val,删除head->next
if (head->next != nullptr && head->next->val == val) {
head->next = head->next->next;
}
head = head->next;
}
ListNode* res = dummy->next;
delete dummy;
return res;
}
};
错误
对于测试用例[1,2,2,1],无法正确处理,最后输出[1,2,1],而不是[1,1]
上诉代码无法正确处理的情况:链表中间(不是从头结点开始)的连续值为val的结点,因为每次只判断了一个结点,也只删除了一个结点后,就令head=head->next,如果连续的结点值均为val,head的值就为val,而后续的操作没有对head的值进行判断,直接默认head的值不为val,所以出现错误。
代码4
基于上面的错误原因做出以下修改:在第二个while循环中,再使用while循环,一直删除值为val的结点,直到遇到值不为val的结点,才令head指向它。
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 判空
if (head == nullptr) {
return head;
}
// 如果头结点为val,往后寻找到第一个不为val的结点
while (head != nullptr && head->val == val) {
head = head->next;
}
// 如果不存在不为val的结点,直接返回null
if (head == nullptr) {
return head;
}
// 存在不为val的结点,才继续下面的操作
// 哑结点
ListNode* dummy = new ListNode(-1);
dummy->next = head;
while (head != nullptr) {
// head->next的值一直为val,就一直删除,直到遇见值不为val的结点
while (head->next != nullptr && head->next->val == val) {
head->next = head->next->next;
}
head = head->next;
}
ListNode* res = dummy->next;
delete dummy;
return res;
}
};
复杂度
n为链表结点数
时间复杂度:O(n)。无论是哪种情况,都需要遍历链表的每个结点,所以时间复杂度为O(n)。
空间复杂度:O(1)。额外占用的内存只有1 个哑结点和2个指针,因此空间复杂度为 O(1)(常数阶)。
优化
头结点可能被删 / 改、空链表拼接、多链表合并的时候需要使用哑结点辅助,但是代码中提前处理了头结点变化的情况,所以无需使用哑结点也能实现。
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if (head == nullptr) {
return head;
}
while (head != nullptr && head->val == val) {
head = head->next;
}
if (head == nullptr) {
return head; }
ListNode* res = head;
while (head != nullptr) {
while (head->next != nullptr && head->next->val == val) {
head->next = head->next->next;
}
head = head->next;
}
return res;
}
};
官方题解
方法一:递归
链表的定义具有递归的性质,因此链表题目经常可以用递归的方法求解。
递归过程:对于给定的链表,首先对除了头结点head以外的结点进行删除操作,然后判断head的结点值是否=val。如果head的结点值=val,则需要删除head,因此删除操作后的头结点变为head.next;如果不相等,则保留head,删除操作后的头结点还是head。
递归的终止条件是head为空,此时直接返回head。当head不为空时,递归地进行删除操作,然后判断head的结点值是否=val,并决定是否要删除head。
代码
以下代码是我看了题解的文字写出来的,和官方的一比较,相形见绌。
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head==nullptr){
return head;
}
ListNode* p=removeElements(head->next,val);
if(head->val==val){
return p;
}
head->next=p;
return head;
}
};
官方的代码简明概要
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if (head == nullptr) {
return head;
}
head->next = removeElements(head->next, val);
return head->val == val ? head->next : head;
}
};
复杂度
n为链表长度
时间复杂度:O(n),递归过程中需要遍历链表一次。
空间复杂度:O(n)。主要取决于递归调用栈,最多不会超过n层。
方法二:迭代
我的思路也是迭代,但是很冗余,官方的就很简明概要,属于是一步到位。
用temp表示当前节点。如果temp的下一节点不为空且下一个节点的结点值等于给定的val,则需要删除下一节点。如果不等于,则保留,将temp移动到下一个节点即可。
当temp的下一个节点为空时,链表遍历结束,此时所有值为val的节点都被删除。
代码
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
struct ListNode* dummy=new ListNode(0,head);
struct ListNode* temp=dummy;
while(temp->next!=NULL){
if(temp->next->val==val){
temp->next=temp->next->next;
}else{
temp=temp->next;
}
}
return dummy->next;
}
};
复杂度
n是链表长度
时间复杂度:O(n)。
空间复杂度:O(1)。