当前位置: 首页 > java >正文

对于单链表相关经典算法题:203. 移除链表元素的解析

开篇介绍:

hello 大家,还记得上一篇文字里,我们一同叩开了链表世界的大门,初探了那些由指针串联起的离散节点如何编织出灵动的数据脉络。如果没看过的,大家可以看这一篇博客:对于数据结构:链表的超详细保姆级解析-CSDN博客

而真正的知识沉淀,从来少不了躬身实践的打磨 —— 正如匠人需在千次锤炼中领悟器物之魂,我们对链表的理解,也需在算法题目的推演中步步深化。

因此,在这篇文字里,我特意甄选了一道单链表领域的经典算法题。它们如同一面精巧的棱镜,既能折射出链表操作的核心逻辑,亦能映照出指针运用的微妙心法。愿我们能一同拆解其中的思路脉络,在代码与逻辑的交响中,让对链表的认知从 “初步识得” 迈向 “熟稔于心”,让那些散落的知识点凝结成解决问题的利刃。

先附上这道题的链接,我们依旧是老样子,大家在看解析之前自行练练手,帮助大家理解题目:
203. 移除链表元素 - 力扣(LeetCode)https://leetcode.cn/problems/remove-linked-list-elements/description/接下来,便让我们共同将这道题斩于马下!!!

203. 移除链表元素:

这道题应该算是学习链表之路上最常见也是最经典、基础的算法题了,我们先看题目:

题意分析:

这道 “203. 移除链表元素” 的题目,我们可以从以下几个方面详细地分析题意:

1. 问题核心

给定单链表的头节点 head 和一个目标整数值 val,需要删除链表中所有值等于 val 的节点,最后返回处理后新的链表头节点。

2. 输入输出理解

  • 输入
    • head:单链表的头节点,通过它可以访问整个链表。链表的每个节点包含一个整数值 val(节点自身存储的值)和指向下一个节点的指针 next
    • val:要从链表中移除的目标值。
  • 输出:经过移除操作后,新的链表头节点。如果原链表中所有节点都被移除(比如原链表所有节点值都为 val),则返回 null(空链表)。

那么在知道了题意之后,大家可以想一想,是否只有这么一些呢?是不是还有一些特殊情况呢?大家可以先自行摸索一下,我会在后面给出答案。

解题思路:

那么针对这道题,我在这边给大家提供两种方法,两个方法都有各自的优势,大家可以都进行参考。

方法一:遍历法

这个方法相对而言,会复杂一些,但是也是一个能够极大程度扩展我们思维以及加深对链表的理解的方法。

我们先看,既然题目要求我们对链表中如果有与指定数据相同的节点进行删除,那么经过上篇博客,大家很容易就能看出,这是对链表指定位置的删除,只不过在本道题中,可能会有多个,不过没事,兵来将挡,水来土掩,我们且战。

首先,我们肯定要对链表进行遍历一番,去一个一个地探查链表中的每一个节点所存储的数据,并与题目指定数据进行比较,如果找到了数据相同的节点,就把这个节点删除掉,然后再去下一个节点继续探查,那么我们便可以得出初步的一个总框架代码:

sl* position = head;
while (position != NULL)
{
if (position->val == val)
{
//执行删除节点操作
}
position = position->next;
}

那么要怎么进行指定节点的删除呢,这个我们在上一篇博客中有进行介绍,这里我就直接给出代码来,大家如果有不明白的地方,可以去上一篇博客进行查缺补漏。

//实现对链表指定位置的删除的函数
void slisterase(slistnode** pphead, slistnode* position)
{//得先判断传来的二级指针是否为空,如果为空的话,谈何删除assert(pphead);//因为要进行,所以我们还得多一个断言,毕竟如果第一个节点都没有内容,谈何删除assert(*pphead);//也可以判断一下我们传入的位置是不是NULLassert(position);//我们来分析一下如何实现对链表指定位置的删除//意思就是我们要把指定位置的那个节点释放掉//但是为了链表的连续性,//这就需要我们把该节点的前一个节点的钥匙指向该节点的下一个节点//这也就意味着,我们需要去找到position的前一个节点//这个时候,pphead的作用就有了,它需要去通过循环去变为指定节点的前一个节点//至于为什么要用二级指针,那是因为我们有可能要进行头删//(即我们指定的节点是链表的第一个节点,那这么一来就得头删)//而头删,是需要二级指针的,我们可以直接调用我们之前写的头删函数//那么问题来了,我们需不需要把头删这个情况单独列出来呢?//答案是需要的,因为我们需要一个新变量去指代position的前一个节点//那要是我们输入的节点是第一个节点的话,那那个新变量不就越界了//会发生啥也找不到的情况//先处理我们指定的节点是第一个节点的情况if (position == (*pphead)){slistpopfront(pphead);//直接调用之前所写的头删函数}//我们指定的节点不是第一个节点的情况else{slistnode* pucr = *pphead;while (pucr->next != position)//当pucr没有到指定位置的前一个节点{pucr = pucr->next;//如果还没到的话,就继续往后找}//出来循环之后,此时的pucr就是position的前一个节点//接着我们就得把pucr的钥匙指向position的下一个节点pucr->next = position->next;//然后我们开始释放positionfree(position);position = NULL;//要理解 “为什么释放一级指针position就能删除对应的节点”,核心在于指针与内存的关系//指针本质是 “地址的容器”,而free函数操作的是指针指向的内存,而非指针本身。//1. 先明确:position是什么?//position是一个一级指针变量,它的作用是存储 “要删除的节点” 在内存中的地址。//比如,假设链表中有一个节点 A,其内存地址是0x123456,那么position变量中就存储着0x123456,即position = &节点A。//2. free(position)的真正含义//free函数的功能是:释放指针所指向的内存块,而不是 “删除指针变量本身”。//当执行free(position)时://函数会找到position中存储的地址(比如0x123456),将这块内存归还给操作系统,//使其成为 “无效内存”(后续无法再访问)。//这意味着,“要删除的节点” 所占用的内存被彻底释放,该节点从内存中消失。//3. 为什么不需要 “二级指针” 来释放节点?//二级指针(如pphead)的作用是修改指针变量本身的值(比如修改头指针的指向)。//而释放节点的核心是回收节点占用的内存,只需要知道 “节点在内存中的地址” //即可 ——position已经存储了这个地址,所以用一级指针足够。}
}

但是本题中不需要用到上述的头删代码,我们会在其他地方进行调整,所以可以忽略:

//先处理我们指定的节点是第一个节点的情况
if (position == (*pphead))
{slistpopfront(pphead);//直接调用之前所写的头删函数
}

这一串代码,所以串联一下两个代码,就是:

void slisterase(slistnode** pphead, slistnode* position)
{assert(pphead);assert(*pphead);assert(position);slistnode* pucr = *pphead;  // 修正指针定义,添加*while (pucr->next != position){pucr = pucr->next;}pucr->next = position->next;free(position);position = NULL;}sl* position = head;
while (position != NULL)
{if (position->val == val){slisterase(head, position);}position = position->next;
}

如此一来,我们便算是解决了对于指定位置节点的删除,可是,仅仅只是这样子吗?那知道经典算法题未免也太小儿科了一些,因此,这道题是有特殊情况的,这个便需要我们的处理。

我们先看一下,这部分代码直接提交会出现什么呢?

可以看到,很明显爆红了,原因是:第 128 行:字符 15:运行时错误:类型为“struct sl”的空指针内的成员访问 [solution.c],这就代表说,我们对空指针进行了访问,而这很明显是不行的,那么到底问题出在了哪里呢?且看下文。

特殊情况1:

我们先看一下题目的这一部分:

大家可不要忽略了一道题目所给的提示,那里很有可能藏着解题时容易被忽略的细节,我们一旦没有发现,就会导致代码无法通过。

这里面告诉我们说,链表中的节点有可能是0个节点,意思就是说,有可能是空链表,那么我们的while(temp->next!=position)可不就是对空节点进行了访问吗?所以编译器肯定会报错,那么我们要怎么处理这种情况呢?很简单,我们在进行指定节点删除前,先判断一下传入的是不是空链表,如果是,就代表没得删,那么我们就直接返回NULL,即如下:

if(head==NULL)
{return NULL;
}

那么经过了这一步完善之后的总代码,即是:

typedef struct ListNode sl;void slbreak(sl* head,sl* position)
{sl* temp=head;while(temp->next!=position){temp=temp->next;}temp->next=position->next;
}struct ListNode* removeElements(struct ListNode* head, int val) 
{if(head==NULL){return NULL;}sl* position=head;while(position!=NULL){if(position->val==val){slbreak(head,position);}position=position->next;}return head;
}

那么问题来了,仅只步于此吗?哈哈,我们提交一下,看能不能通过,有可能题目就是这么简单呢,

很可惜,还是报错了,而且还换了个地方报错,上次报错是在struct ListNode* removeElements(struct ListNode* head, int val)中,而这次报错是在void slbreak(sl* head,sl* position)这个我们对指定位置删除的函数中,这是为什么呢?我们看特殊情况2.

特殊情况2:

我们看一下上图中,最后执行的输入,因为出现各这个就代表说我们写的代码是在进行这一个链表的时候,出现报错:

可以看到,在这一个示例中,第一个节点就是我们要删除的节点,那么大家应该一下子就明白了,这个时候就不能用指定位置删除这个函数了,因为这个函数是需要找到指定节点的前一个节点的,而我们的第一个节点,哪里来的前一个节点呢?所以我们应该要进行头删,那么要怎么头删呢?

我们知道,在上一篇博客中讲述的头删函数,它是只能进行一次的,我们如果想要多次进行头删,是需要多次调用的,那么当面对题目示例这种每个节点都是要进行删除的情况呢,我们知道,头删时,原先链表的第二个节点会变为新链表的第一个节点,那么我们就又需要进行头删,直到节点全部清空或者到了不为相同数据的节点,对此,我们要怎么办呢?

其实也不难,我们可以借助while循环进行逐个打击,判断条件就是当节点中数据==题目指定数据的时候,只要还满足这个条件,我们就进行头删,头删的代码大家应该不陌生,我这边直接给出:

while (head->val == val)  // 如果第一个节点就是目标值,无法找到前一个节点,需单独处理
{head = head->next;  // 不断将下一个节点变为新的头节点
}

到了这里,大功告成了吗?很遗憾的告诉大家,并没有,那么还差在哪里呢?我们不妨再提交一次:

又是访问空指针的报错,这次是对while (head->val == val)这一行的报错,那么这是为什么呢?

大家不妨想一想,我们对while循环的判断条件是while (head->val == val),那么大家有没有想过,如果链表中的节点都经过头删删除掉了呢?由于我们不是设置的while (head!= NULL),所以即使节点都被删除了,编译器还是对进行访问,(⊙o⊙)!!!那很明显,又空指针访问了,所以,为了避免这种情况,我们就得在while循环中加一个if判断语句,如果进过所有删除后,head为空了,那我们就直接返回NULL,具体如下:

while (head->val == val)  // 如果第一个节点就是目标值,无法找到前一个节点,需单独处理
{head = head->next;  // 不断将下一个节点变为新的头节点if(head==NULL)//如果经过头删后变为空链表了,就直接返回NULL{return NULL;}
}

到了这里,这道题的第一个解法才算是无缺无漏了

完整代码:

不过在这里,我先给大家提个醒,有人看完了解析后,可能会写出这么一个代码来:

struct ListNode* removeElements(struct ListNode* head, int val) 
{if(head==NULL){return NULL;}else if(head->val==val){while(head->val==val){head=head->next;//不断把第二个节点变为第一个节点if(head==NULL)//如果,head到了最后一个节点的next(即NULL),那么这个时候,它是无法跳出循环的,因为我们的循环条件是head->val==val,它是需要访问head的,但是head一旦为NULL,是无法访问的,这就会导致空指针访问{return NULL;}}}else{sl* position=head;while(position!=NULL){if(position->val==val){slbreak(head,position);}position=position->next;}}return head;
}

乍一看这代码,没什么问题,可是一提交,就是不行,那么问题出在哪里呢?其实,就出现在那个if、else if和else中,我们要知道,当我们这么使用的时候,编译器是会只运行其中的一种情况的,比如条件满足的是if语句,那么编译器就会只运行if语句中的代码,至于else if和else语句中的代码,编译器是直接忽略掉的,那么这么一来,很明显就有漏洞了,当出现题目为{4,5,4},val==4时,程序无法将4的全部删除,最后只会变为{5,4},因为编译器只会运行一个情况的代码。

所以,我们就不能如上那么写,直接在开头先写上判断是否为空链表的判断语句,再在其下面写上头删,最后再写指定位置删除,大家要注意,这里3个的顺序,同样不能乱,乱了就会发生报错,至于为什么,那是因为编译器是从上往下执行代码的。

下面给出完整代码:

typedef struct ListNode sl;void slbreak(sl* head,sl* position)
{sl* temp=head;while(temp->next!=position){temp=temp->next;}temp->next=position->next;
}struct ListNode* removeElements(struct ListNode* head, int val) 
{if(head==NULL){return NULL;}while (head->val == val)  // 如果第一个节点就是目标值,无法找到前一个节点,需单独处理
{head = head->next;  // 不断将下一个节点变为新的头节点if(head==NULL){return NULL;}
}sl* position=head;while(position!=NULL){if(position->val==val){slbreak(head,position);}position=position->next;}return head;
}

再给上详细注释版本:

// 假设结构体定义如下(通常在头文件中):
// struct ListNode {
//     int val;               // 节点存储的值
//     struct ListNode *next; // 指向下一个节点的指针
// };// 为结构体ListNode定义别名sl,简化后续代码书写
typedef struct ListNode sl;/*** 函数名称:slbreak* 功能描述:从链表中删除指定节点position* 参数说明:*   head:链表的头节点(用于定位position的前一个节点)*   position:需要被删除的节点* 实现逻辑:通过遍历找到position的前一个节点,将其next指针指向position的下一个节点* 示例:*   原链表:1 -> 2 -> 3 -> 4*   调用slbreak(head, 指向3的节点)后:*   结果链表:1 -> 2 -> 4*/
void slbreak(sl* head, sl* position)
{// 定义临时指针temp,从链表头部开始遍历sl* temp = head;// 遍历链表,直到找到position的前一个节点(即temp的next指向position)// 示例:要删除3时,temp会停在2的位置(因为2->next是3)while (temp->next != position){temp = temp->next;  // 移动到下一个节点}// 将前一个节点的next指向position的下一个节点,完成删除// 示例:2->next原本指向3,现在改为指向3->next(即4)temp->next = position->next;
}/*** 函数名称:removeElements* 功能描述:移除链表中所有值等于val的节点,并返回处理后的链表头节点* 参数说明:*   head:原链表的头节点*   val:需要被移除的目标值* 返回值:处理后的链表头节点(可能与原头节点不同)* 示例:*   原链表:1 -> 2 -> 6 -> 3 -> 6 -> 4*   调用removeElements(head, 6)后:*   结果链表:1 -> 2 -> 3 -> 4*/
struct ListNode* removeElements(struct ListNode* head, int val) 
{// 情况1:如果链表为空(头节点为NULL),直接返回NULL// 示例:head = NULL时,无需处理,返回NULLif (head == NULL){return NULL;}// 情况2:单独处理头节点值等于val的情况// 因为头节点没有前一个节点,需要特殊处理// 示例:原链表6 -> 6 -> 1 -> 2,val=6时:// 第一次循环:head指向第2个6// 第二次循环:head指向1(此时1->val≠6,退出循环)while (head->val == val)  {// 将头节点后移一位,相当于删除当前头节点head = head->next;  // 如果头节点移到了NULL(所有节点都被删除),直接返回NULL// 示例:原链表6 -> 6 -> 6,val=6时,最终head会变为NULLif (head == NULL){return NULL;}}// 情况3:处理链表中剩余节点(非头节点)// 定义遍历指针position,从处理后的头节点开始遍历sl* position = head;// 遍历链表,直到所有节点处理完毕// 示例:当前链表1 -> 2 -> 6 -> 3 -> 6 -> 4,position初始指向1while (position != NULL){// 如果当前节点的值等于目标值val// 示例:当position指向6时,满足条件if (position->val == val){// 调用slbreak函数删除当前节点// 示例:删除第一个6后,链表变为1->2->3->6->4slbreak(head, position);}// 移动到下一个节点,继续遍历// 示例:position从1→2→6(被删除)→3→6(被删除)→4→NULLposition = position->next;}// 返回处理后的链表头节点// 示例:最终返回指向1的头节点,链表为1->2->3->4return head;
}

到此,第一个方法大功告成。

方法二:创建新链表

方法二可以说是一个很绝妙的方法,同时它的适用性、通用性也非常高。

我们方法一是在原链表中进行操作、遍历,大家也看到了,还是较为麻烦的,那么,我们是不是可以把原链表中数据不为val的节点全部都传进新链表中呢?最后我们只需要把这个新链表传回即可。

那么,我们要怎么创建新链表呢?

其实并不难,关键在于,我们思维的转换。且听我道来

首先,我们要创建一个头结点newhead,它是我们所创建的新链表的头结点,也是我们后续要返回的节点,那么问题来了,我们返回是返回newhead,但是我们要返回的应该是头结点,因为只有返回一个链表的头结点,那么才能将这个链表的每个节点都遍历并打印每个节点所存储的数据。

但是我们创建的新链表肯定是要依次存储新数据的,那么只有一个newhead的话,我们肯定就需要newhead去移动存储数据,这样子的话,存储完所有的节点之后,newhead就肯定不会是指向我们所创建的新链表的头结点了,有可能会是尾节点。

所以,一个不够,那我们就再创建一个newtail,它去负责移动以及存储新节点,而newhead只需要老老实实的存储好头节点就行了,这么一来,最后传回newhead,就是我们所创建的新链表的头结点了。

那么具体要怎么书写代码呢?

首先,我们要创建两个新指针(节点):

sl* newhead=NULL;
sl* newtail=NULL;

接着,我们就得分情况讨论了,因为我们创建的新链表很明显是一个空链表,那这个时候直接在newhead后面插入数据肯定是不行,所以我们就得分两种情况:

第一种情况:newhead为NULL(即为空链表)时,那我们就得对空链表进行头插,就是将原链表的节点直接赋值给newhead,如此一来,newhead就不是空指针了,但是仅仅只是如此吗?很明显,不够,我们光光给newhead好东西了,那还有个newtail呢,我们怎么可能视而不见,搞偏心呢?但是我们要对newtail怎么处理呢?也将原链表的节点直接赋值给它吗?这行吗?答案是:不行。

为什么呢?首先,我们要明确newtail的作用是不断移动到下一个节点并能够存储数据到新链表中,但是newtail本身其实是没有存储任何数据的,它的用途只是给原链表的节点指方向,指引它们不断地在新链表中一个接着一个的往后存储,直到结束。

那我们都知道,一个节点中的next是要指向其下一个节点的,假设说这个节点是newhead,那么就是说newhead的next指针要指向下一个节点,而我们又知道,newhead是不能动的,且newtail要承担起指路者的作用,但是我们又要让newhead的next指针能够指向下一个节点,这可怎么办呢?其实答案已经呼之欲出了,我们直接让newtail去=newhead就行了,这么一来,既能实现newtail指路者的作用,又能避免newhead的移动,

所以,对于第一种情况,我们的详细代码是:

sl* newhead = NULL;
sl* newtail = newhead;
sl* temp = head;
while (temp != NULL)
{if (temp->val != val){if (newhead == NULL)  // 当新链表为空时,直接插入第一个节点{newhead = temp;newtail = newhead;}// 此处缺少else分支,用于处理非第一个节点的插入逻辑}temp = temp->next;
}

那么第二种情况,就是当newhead不为空(即为非空链表)的时候,那么这个时候,我们就不能再动newhead了(因为newhead已经在链表为空的情况下进行处理了),我们要在这里发挥newtail的作用,那么要怎么实现呢?

首先我们知道在newhead为NULL(即为空链表)时,我们已经让newtail=newhead了,那么这个时候我们想要在newhead后面插入新节点的话,就把把这个新节点的地址传递给newhead的next指针,但是又因为我们是要依靠newtail,而且newtail已经=newhead了,所以我们可以写newtail->next=temp ,这么一来,我们就可以把要加入的新节点传给newhead的next指针了,同时也实现了串联,但是仅仅只是newtail->next=temp这一句代码,还不够,因为我们有时候还是需要对于新链表第二个、第三个等等的节点继续插入,那我们光光newtail->next=temp肯定不够。

在这里我们假设要对新链表的第二个节点后插入新链表的第三个节点,那我们首先肯定还是要将第二个节点的next指针指向第三个节点,即newtail->next=temp,但是问题也随之而来,我们的newtail还是=newhead的,那么直接newtail->next=temp,不就相当于是对头节点之后插入第二个节点吗?这样子肯定是不行的,正确的流程应该是在对新链表的第二个节点后插入新链表的第三个节点的时候,我们的newtail就应该已经是第二个节点了,这样子在进行newtail->next=temp时,才能把第二个节点的next指针指向第三个节点。

所以,为了达到这个目的,我们就得在newtail->next=temp之后,对newtail进行更新,即让newtail变为新加入的那一个节点,即newtail=newtail->next,这么一来才能让后续的插入正常且正确的运行,下面是详细代码:

{newtail->next=temp;//直接将原链表的节点赋值给新链表的新节点newtail=newtail->next;//直接可以将原节点的下一个节点给覆盖成新的非val的节点
}

如此,才算是大功告成,下面给出完整的使用新链表接收节点的代码(不是本题解答代码):

sl* newhead = NULL;
sl* newtail = newhead;
sl* temp = head;
while (temp != NULL)
{if (newhead == NULL)  // 当新链表为空时,直接插入第一个节点{newhead = temp;newtail = newhead;}else{newtail->next = temp;newtail = newtail->next;}temp = temp->next;
}

下面再给大家一个示例,让大家了解的更加明白,大家同时也可以画图去理解哦:

我们用一个具体的链表例子,详细分析这段代码的每一步执行过程。假设原链表为 3 -> 1 -> 4 -> 2(节点值依次为 3、1、4、2,最后节点的nextNULL),代码的功能是将原链表的所有节点 “复制” 到新链表中(通过指针指向实现)。

初始状态
  • 原链表结构:head 指向第一个节点(值为 3),节点关系为:
    3 -> 1 -> 4 -> 2 -> NULL
  • 代码中指针的初始值:
    newhead = NULL(新链表头指针,初始为空)
    newtail = newhead(新链表尾指针,初始也为空)
    temp = head(遍历指针,从原链表头开始,初始指向值为 3 的节点)

步骤 1:第一次进入循环(temp 指向值为 3 的节点)

  • 循环条件:temp != NULL(成立,因为temp指向 3 的节点)。
  • 执行循环体:
    • 判断 newhead == NULL(成立,新链表为空),进入if分支:
      • newhead = temp → newhead 指向值为 3 的节点(新链表的头节点是 3)。
      • newtail = newhead → newtail 也指向值为 3 的节点(新链表的尾节点也是 3)。
    • 执行 temp = temp->next → temp 指向原链表的下一个节点(值为 1 的节点)。

当前状态

  • 新链表:3 -> ?newheadnewtail都指向 3,3 的next暂时还是原链表的 1)。
  • temp 指向值为 1 的节点。

步骤 2:第二次进入循环(temp 指向值为 1 的节点)

  • 循环条件:temp != NULL(成立)。
  • 执行循环体:
    • 判断 newhead == NULL(不成立,新链表已有节点 3),进入else分支:
      • newtail->next = temp → 新链表尾节点(3)的next指向temp(1 的节点),新链表变为 3 -> 1
      • newtail = newtail->next → newtail 移动到新的尾节点(1 的节点)。
    • 执行 temp = temp->next → temp 指向原链表的下一个节点(值为 4 的节点)。

当前状态

  • 新链表:3 -> 1 -> ?newhead指向 3,newtail指向 1,1 的next是原链表的 4)。
  • temp 指向值为 4 的节点。

步骤 3:第三次进入循环(temp 指向值为 4 的节点)

  • 循环条件:temp != NULL(成立)。
  • 执行循环体:
    • 判断 newhead == NULL(不成立),进入else分支:
      • newtail->next = temp → 新链表尾节点(1)的next指向temp(4 的节点),新链表变为 3 -> 1 -> 4
      • newtail = newtail->next → newtail 移动到新的尾节点(4 的节点)。
    • 执行 temp = temp->next → temp 指向原链表的下一个节点(值为 2 的节点)。

当前状态

  • 新链表:3 -> 1 -> 4 -> ?newhead指向 3,newtail指向 4,4 的next是原链表的 2)。
  • temp 指向值为 2 的节点。

步骤 4:第四次进入循环(temp 指向值为 2 的节点)

  • 循环条件:temp != NULL(成立)。
  • 执行循环体:
    • 判断 newhead == NULL(不成立),进入else分支:
      • newtail->next = temp → 新链表尾节点(4)的next指向temp(2 的节点),新链表变为 3 -> 1 -> 4 -> 2
      • newtail = newtail->next → newtail 移动到新的尾节点(2 的节点)。
    • 执行 temp = temp->next → temp 指向原链表的下一个节点(NULL,因为 2 是最后一个节点)。

当前状态

  • 新链表:3 -> 1 -> 4 -> 2 -> NULL(因为原链表中 2 的next就是NULL)。
  • temp 变为NULL

步骤 5:退出循环

  • 循环条件:temp != NULL(不成立,因为tempNULL),退出while循环。

最终结果

  • newhead 指向新链表的头节点(值为 3 的节点),新链表的结构与原链表完全一致:3 -> 1 -> 4 -> 2 -> NULL
  • newtail 指向新链表的尾节点(值为 2 的节点)。

逻辑总结

这段代码的核心是通过 头插法处理第一个节点,尾插法处理后续节点,将原链表的所有节点 “迁移” 到新链表中(本质是指针重新指向,而非创建新节点)。

  • 当新链表为空(newhead == NULL)时,直接将第一个节点作为新链表的头和尾。
  • 当新链表非空时,通过newtail指针将新节点接在链表尾部,并更新newtail的位置。
  • 最终得到一个与原链表结构完全相同的新链表(共享原节点的内存,并非复制)。

到这里,我们算是初步完成了创建新链表这个方法,如下:

sl* newhead = NULL;
sl* newtail = newhead;
sl* temp = head;
while (temp != NULL)
{if (temp->val != val){if (newhead == NULL)  // 当新链表为空时,直接插入第一个节点{newhead = temp;newtail = newhead;}else{newtail->next = temp;newtail = newtail->next;}}temp = temp->next;
}

但是,这些代码还不够,首先要加的就是对原链表是否为空链表的判断,如果是空链表就直接返回NULL,不过这只是一小点,我们还要在while循环后面再加一些代码,具体是什么呢?

大家想一下这个情况:

这个示例中,最后一个节点的数据也是val,那么我们就肯定不会把这最后一个节点存储到新链表中,可随之而来的就是一个大问题,如上面这个示例,我们新链表中的最后一个节点是原链表的倒二个节点,但是呢,这一个节点的next指针(即),可是还存储着原链表的最后一个节点的地址的,那么这么一来,新链表真正的最后一个节点其实是原链表的最后一个节点。

所以,为了避免这个情况,我们就得把newtail->next置为空(因为经过循环,newtail已经变成了新链表最后一个节点),这样子新链表的最后一个节点的next指针就不会存储原链表的最后一个节点了,但是我们只是newtail->next=NULL这么写吗?

仅仅这样子,还是不行,因为万一原链表的最后一个节点的数据不是val呢,那么这个时候的newtail其实就是原链表的最后一个节点的next指针了,是为NULL的,那我们此时再newtail->next=NULL,是相当于对空指针进行访问的,是不行的。

所以,我们在进行newtail->next=NULL之前,还要先用if语句判断newtail是否为空指针,不是了再进行newtail->next=NULL,否则就不执行newtail->next=NULL操作。

到这里,我们才算是把这个方法的坑都填了,我们下面看方法二的完整代码:

创建新链表做法:typedef struct ListNode sl;struct ListNode* removeElements(struct ListNode* head, int val) 
{if(head==NULL){return NULL;}sl* newhead=NULL;sl* newtail=newhead;sl* temp=head;while(temp!=NULL){if(temp->val!=val){if(newhead==NULL)//当新链表第一个节点就为空指针时,要直接头插{newhead=temp;newtail=newhead;}else{newtail->next=temp;//直接将原链表的节点赋值给新链表的新节点newtail=newtail->next;//直接可以将原节点的下一个节点给覆盖成新的非val的节点}}temp=temp->next;}//要注意,如题目所给示例一般,如果最后一个节点也是val,就不能将原节点的下一个节点给覆盖成新的非val的节点,因为停止循环了,所以需要我们自己置空// newtail=NULL;//直接置空也不行,因为从newhead出发的倒二个节点的next还是存储着原先的最后一个节点if(newtail!=NULL)//出来循环之后的newtail其实是类似题目图片5的地址,之所以我们要判断了newtail节点不为空之后再去进行下一个节点为空,是因为有可能会出现出来之后的新链表还是空链表的情况,会发生空指针访问{newtail->next=NULL;}return newhead;
}

再给上详细注释版本:

// 假设链表节点结构体定义如下:
// struct ListNode {
//     int val;               // 节点存储的值
//     struct ListNode *next; // 指向下一个节点的指针
// };// 为结构体ListNode定义别名sl,简化代码书写
typedef struct ListNode sl;/*** 函数名称:removeElements* 功能描述:移除链表中所有值等于val的节点,返回新链表头节点* 实现方法:创建新链表,仅保留原链表中值不等于val的节点* 参数:*   head - 原链表的头节点(例如:1->6->2->6->3)*   val  - 需要移除的目标值(例如:6)* 返回值:*   处理后的新链表头节点(例如:1->2->3)*/
struct ListNode* removeElements(struct ListNode* head, int val) 
{// 情况1:如果原链表为空(head是NULL),直接返回NULL// 示例:原链表为NULL,val=6 → 返回NULLif(head == NULL){return NULL;}// 定义新链表的头指针和尾指针sl* newhead = NULL;  // 新链表的头节点,初始为空(还没有节点)sl* newtail = newhead;  // 新链表的尾节点,初始与头节点相同(均为NULL)// 定义遍历指针temp,用于遍历原链表sl* temp = head;  // 从原链表的头节点开始遍历(示例中初始指向值为1的节点)// 遍历原链表的所有节点(直到temp变为NULL)// 示例:原链表节点顺序为1→6→2→6→3→NULLwhile(temp != NULL){// 判断当前节点的值是否不等于目标值val(需要保留该节点)// 示例中val=6,当前节点值不等于6时进入分支if(temp->val != val){// 子情况1:新链表还是空链表(newhead为NULL,尚未添加任何节点)// 示例:第一次循环时newhead是NULLif(newhead == NULL){// 将当前节点作为新链表的第一个节点newhead = temp;  // 示例:newhead指向值为1的节点newtail = newhead;  // 示例:newtail也指向值为1的节点(新链表此时为1)}// 子情况2:新链表已有节点(需要在尾部添加当前节点)else{// 将当前节点链接到新链表的尾部newtail->next = temp;  // 示例:第二次符合条件时(节点2),1->next指向2newtail = newtail->next;  // 示例:newtail移动到节点2(新链表变为1->2)}}// 无论当前节点是否被保留,都移动到原链表的下一个节点// 示例遍历顺序:1→6→2→6→3→NULLtemp = temp->next;}// 关键处理:确保新链表的尾部正确终止// 问题场景:原链表中最后一个保留节点的next可能指向被删除的节点// 示例:原链表中3的next原本指向NULL(无需处理),但如果原链表是1->6->2:// 新链表保留1->2,而2的next原本指向6(被删除),需手动置为NULLif(newtail != NULL)  // 防止新链表为空时访问空指针(如原链表全是val的情况){newtail->next = NULL;  // 示例:若新链表是1->2->3,3->next被置为NULL}// 返回新链表的头节点// 示例:最终返回指向1的指针,新链表为1->2->3->NULLreturn newhead;
}

详细执行步骤示例(以原链表1->6->2->6->3val=6为例)

初始状态
  • 原链表:1 -> 6 -> 2 -> 6 -> 3 -> NULL
  • 指针初始值:newhead=NULLnewtail=NULLtemp=head(指向 1)
第 1 次循环(temp 指向 1)
  • 判断 temp->val != 6(1≠6,成立)
  • 因 newhead=NULL(新链表为空):
    • newhead = temp → newhead指向 1
    • newtail = newhead → newtail指向 1
  • temp = temp->next → temp指向 6
第 2 次循环(temp 指向 6)
  • 判断 temp->val != 6(6≠6,不成立)
  • 不执行任何操作
  • temp = temp->next → temp指向 2
第 3 次循环(temp 指向 2)
  • 判断 temp->val != 6(2≠6,成立)
  • 因 newhead≠NULL(新链表已有节点 1):
    • newtail->next = temp → 1 的 next 指向 2(新链表变为1->2
    • newtail = newtail->next → newtail指向 2
  • temp = temp->next → temp指向 6
第 4 次循环(temp 指向 6)
  • 判断 temp->val != 6(6≠6,不成立)
  • 不执行任何操作
  • temp = temp->next → temp指向 3
第 5 次循环(temp 指向 3)
  • 判断 temp->val != 6(3≠6,成立)
  • 因 newhead≠NULL
    • newtail->next = temp → 2 的 next 指向 3(新链表变为1->2->3
    • newtail = newtail->next → newtail指向 3
  • temp = temp->next → temp指向 NULL(退出循环)
最终处理
  • 执行 newtail->next = NULL → 3 的 next 被置为 NULL(确保链表正确结束)
  • 返回 newhead → 新链表为1->2->3->NULL

关键逻辑说明

  1. 新链表构建:通过newheadnewtail分别记录新链表的头和尾,只添加符合条件的节点
  2. 边界处理
    • 空链表直接返回 NULL
    • 新链表为空时newtail->next=NULL会导致空指针错误,因此需要判断newtail!=NULL
  3. 链表终止:必须手动将新链表尾节点的 next 置为 NULL,否则可能残留原链表中被删除的节点引用

这种方法通过 "筛选 + 重建" 的思路,避免了在原链表中删除节点时需要查找前驱节点的复杂操作,逻辑更直观易懂。

至此,大功告成。

结语:

当最后一行代码通过编译,当测试用例全部亮起绿灯,你或许会和我一样,对着屏幕露出释然的微笑。这道看似简单的 "移除链表元素",实则藏着数据结构世界最珍贵的成长密码 —— 那些关于指针的纠结、边界条件的挣扎、逻辑漏洞的排查,恰恰是理解链表本质的必经之路。

两种解法,恰似面对问题的两种思维:遍历删除法如外科手术,精准操作却需步步谨慎,教会我们敬畏每一个指针的指向;新链表法则像重建家园,另辟蹊径中彰显化繁为简的智慧,让我们明白换个角度往往柳暗花明。而那些踩过的坑 —— 空指针的访问、头节点的特殊处理、链表终止的边界条件 —— 早已悄悄化作我们对细节的敏感度。

数据结构的学习从来不是一蹴而就的神话。就像链表的节点需要逐个串联,我们的认知也需要在这样一题一解的积累中慢慢成形。今天能从容处理链表的删除,明天面对二叉树的遍历便多一分底气;此刻为指针逻辑辗转反侧,未来设计复杂算法时便少一分迷茫。

所以不必急于求成,也不必畏惧犯错。每一次调试时的眉头紧锁,每一次顿悟后的豁然开朗,都是成长最清晰的刻度。当你回头再看这道题,会发现真正收获的不仅是两行通过的代码,更是面对复杂问题时那份拆解逻辑、直击本质的勇气。

下一道题还在前方等待,而你早已比昨天更强大。带着这份对细节的执着与对逻辑的敬畏,继续往前走吧 —— 数据结构的世界里,每一步扎实的脚印,都在为未来的高楼大厦奠定基石。

http://www.xdnf.cn/news/20207.html

相关文章:

  • 数据结构:栈和队列力扣算法题
  • 空域属不属于自然资源?(GPT5)
  • Redis-事务与管道
  • 使用CI/CD部署后端项目(gin)
  • 因泰立科技:用激光雷达重塑智能工厂物流生态
  • 【网安基础】--ip地址与子网掩码
  • 告别线缆束缚!AirDroid Cast 多端投屏,让分享更自由
  • 编写后端JAR包蓝绿发布脚本
  • 23种设计模式——代理模式(Proxy Pattern)详解
  • 【使用goto统计输入数的奇偶数量】2022-10-28
  • 人工智能时代职能科室降本增效KPI设定全流程与思路考察
  • 【高分论文密码】大尺度空间模拟与不确定性分析及数字制图技术应用
  • 为什么动态视频业务内容不可以被CDN静态缓存?
  • [ubuntu][C++]onnxruntime安装cpu版本后测试代码
  • 扫描件、PDF、图片都能比对!让文档差异无所遁形
  • TDengine 时间函数 TODAY() 用户手册
  • Next.js 介绍:为什么选择它来构建你的下一个 Web 应用?
  • 开发环境 之 编辑器、编译器、IDE梳理
  • 深度解读:PSPNet(Pyramid Scene Parsing Network) — 用金字塔池化把“场景理解”装进分割网络
  • 【c++】c++第一课:命名空间
  • uni-app 项目 iOS 上架效率优化 从工具选择到流程改进的实战经验
  • 从零开始的python学习——字典
  • 永磁同步电机负载估计算法--非线性扩张状态观测器
  • 看见世界的另一种可能:Deepoc星眸(StarGaze)如何为视障生活带来曙光
  • Onlyoffice集成与AI交互操作指引(Iframe版)
  • 美团发布 | LongCat-Flash最全解读,硬刚GPT-4.1、Kimi!
  • 标签系统的架构设计与实现
  • Oracle软件在主机平台的应用(课程下载)
  • 请求超过Spring线程池的最大线程(处理逻辑)
  • 企业级项目管理方法设计指南