LeetCode算法题 (移除链表元素)Day15!!!C/C++

https://leetcode.cn/problems/remove-linked-list-elements/description/

一、题目分析

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

今天的题目非常好理解,也就是要删除掉链表中==val的值,并返回新的头节点。

二、相关知识了解

链表这种数据结构其实与数组相似,同属线性表,但是与数组相比的话,链表是不支持随机访问元素的,也就是说要想在链表中查询一个元素的位置的话,时间复杂度最坏为O(n)如果要查找的元素位于链表末尾(或不存在),需要遍历整个链表,遍历次数为n次 。)。平均也就是O((n + 1)/ 2) (这里就不过多的推导了,感兴趣的同学可以期待一下下一期的题目,我会详细带着大家一起设计出一个功能相对完善的链表

对比数组的随机访问

  • 数组支持随机访问(通过下标),查询时间为 O(1),这是链表与数组的核心差异之一。

  • 但链表在插入/删除节点(尤其头部)时更高效 O(1),而数组可能需要移动元素(O(n))。

三、示例分析

复制代码
输入: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
输出:[]

四、解题思路&代码实现

方法一:原链表删除元素(复杂度为O(n))

首先,拿到一个链表第一步我们要进行的是判断该链表是否为空,如果为空的话直接return head就好。其次就是判断当前节点的值是否==val。如果==的话那么就需要对该节点进行删除操作。下面看一下具体代码的实现。

cpp 复制代码
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 处理头节点等于val的情况(可能需要连续删除多个头节点 如示例3)
        while (head && head->val == val) {
            ListNode* temp = head;  // 临时保存当前头节点
            head = head->next;      // 头节点指向下一个节点
            delete temp;           // 释放原头节点的内存
        }

        // 遍历链表,删除非头节点中值等于val的节点
        ListNode* cur = head;       // 当前节点指针
        while (cur && cur->next) {  // 当前节点和下一个节点均非空时循环
            if (cur->next->val == val) {
                // 如果下一个节点值等于val,则删除它
                ListNode* temp = cur->next;  // 临时保存待删除节点
                cur->next = cur->next->next;  // 跳过待删除节点,直接连接下下个节点
                delete temp;                  // 释放待删除节点的内存
            } else {
                // 否则,移动到下一个节点继续检查
                cur = cur->next;
            }
        }

        return head;  // 返回处理后的链表头节点
    }
};

这里需要注意几个关键点:

  1. 首先在C/C++中堆区开辟的空间需进行手动释放,以免造成空间浪费,或者该空间内的脏数据影响程序结果。(一定要养成良好的编程习惯)
  2. 在第二个while语句终止条件中,一定要cur和cur->next都不为空时我们再去进行循环体,确保不会访问到空指针。最终返回的head节点有可能为NULL,也就是示例3的情况。
  3. 在进行头节点的处理使用while 处理连续多个头节点值等于 val 的情况而不是if(if只能处理一次) (例如 [1,1,1,2] 删除 1)。每次删除节点时记得更新head节点,并使用delete释放内存
  4. 如果 cur->next 的值等于 val,则修改 cur->next 跳过该节点,并释放内存。如果不等,则正常移动到下一个节点。

方法二:虚拟头节点法(复杂度为O(n))

虚拟头节点法属于是对法一进行的一个优化操作,可以显著简化链表操作,尤其是在处理涉及头节点删除或修改的问题时。

对比法一的优点:

  1. 无需特殊处理头节点,虚拟头节点始终作为链表的"前置节点",使得真正的头节点(dummy->next)和其他节点的删除逻辑完全一致,避免分支判断。
  2. 简化代码的结构,法一需要进行两步操作,需先对头节点进行预处理操作,再去处理其余节点。而使用虚拟头节点则只需要一个循环即可处理所有节点,避免代码冗余从而简化代码。
  3. 虚拟头节点确保 cur 初始指向一个非空节点(dummy),因此 cur->next 的访问总是安全的(无需额外判空)。

具体实现代码如下:

cpp 复制代码
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 创建虚拟头节点(dummy node),其值任意(这里设为0),next指向原链表头节点
        // 使用虚拟头节点可以统一处理原头节点和其他节点的删除逻辑
        ListNode* dummy = new ListNode(0);
        dummy->next = head;  // 将虚拟头节点连接到原链表
        
        // cur指针用于遍历链表,初始指向虚拟头节点
        ListNode* cur = dummy;
        
        // 遍历链表,检查每个节点的下一个节点(cur->next)
        while (cur->next) {
            if (cur->next->val == val) {  // 如果下一个节点的值等于目标值
                ListNode* temp = cur->next;  // 临时保存要删除的节点
                cur->next = cur->next->next; // 跳过要删除的节点,直接连接下下个节点
                delete temp;                 // 释放要删除节点的内存(避免内存泄漏)
                
                // 注意:这里不移动cur指针,因为新的cur->next可能也需要删除
                // 例如链表[1,2,2,3],删除2时需要连续检查
            } else {
                cur = cur->next;  // 如果不需要删除,则正常移动到下一个节点
            }
        }
        
        // 返回处理后的链表头节点(即dummy->next)
        // 注意:原头节点可能已被删除,dummy->next会自动更新为新的头节点
        ListNode* newHead = dummy->next;
        delete dummy;  // 释放虚拟头节点的内存
        return newHead;
    }
};

关键点说明:

  1. 虚拟头节点:创建一个虚拟头节点作为原链表的起始点,其next指针指向的为原链表的head节点 (作用:统一处理逻辑,避免单独进行头节点删除操作)
  2. 返回值:这里需要注意我们需要返回的值应为虚拟头节点的next,若原链表所有节点都已被删除,那么虚拟头节点的next会变为NULL,则无需处理。

其余与方法一相同!

至此算法已经是最优解!完结撒花!!!🌸🌸🌸

四、题目总结

方法一:原链表删除元素

此方法先对头节点进行处理,若头节点的值等于 val,则连续删除头节点直至其值不等于 val。之后遍历链表,对非头节点中值等于 val 的节点进行删除操作。该方法时间复杂度为 O(n),不过在处理头节点时需要额外的逻辑判断,且要分别处理头节点和非头节点的删除情况,代码相对复杂。

方法二:虚拟头节点法

这种方法创建了一个虚拟头节点,将其 next 指针指向原链表的头节点。通过遍历链表,检查每个节点的下一个节点,若其值等于 val 则进行删除。此方法的优点在于统一了头节点和其他节点的删除逻辑,避免了额外的分支判断,简化了代码结构。时间复杂度同样为 O(n),是对方法一的优化。

总结

在处理链表删除操作时,使用虚拟头节点能有效简化代码,避免因头节点的特殊情况而增加的复杂逻辑,提高代码的可读性和可维护性。两种方法的时间复杂度均为线性,已达到最优解。在实际编程中,要养成手动释放堆区内存的习惯,防止内存泄漏。今天的题解分享到这里,谢谢大家!!!荆轲刺秦!!!

相关推荐
zm1 小时前
五一假期作业
数据结构·算法
长长同学2 小时前
基于C++实现的深度学习(cnn/svm)分类器Demo
c++·深度学习·cnn
csdn_aspnet2 小时前
C# 检查某个点是否存在于圆扇区内(Check whether a point exists in circle sector or not)
算法·c#
杭州的平湖秋月2 小时前
C++ 中 virtual 的作用
c++
泪光29293 小时前
科创大赛——知识点复习【c++】——第一篇
开发语言·c++
梁下轻语的秋缘3 小时前
C/C++滑动窗口算法深度解析与实战指南
c语言·c++·算法
hallo-ooo3 小时前
【C/C++】函数模板
c语言·c++
iFulling3 小时前
【数据结构】第八章:排序
数据结构·算法
一只鱼^_3 小时前
力扣第448场周赛
数据结构·c++·算法·leetcode·数学建模·动态规划·迭代加深
学生小羊3 小时前
[C++] 小游戏 决战苍穹
c++·stm32·单片机