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。如果==的话那么就需要对该节点进行删除操作。下面看一下具体代码的实现。
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; // 返回处理后的链表头节点}
};
这里需要注意几个关键点:
- 首先在C/C++中堆区开辟的空间需进行手动释放,以免造成空间浪费,或者该空间内的脏数据影响程序结果。(一定要养成良好的编程习惯)
- 在第二个while语句终止条件中,一定要cur和cur->next都不为空时我们再去进行循环体,确保不会访问到空指针。最终返回的head节点有可能为NULL,也就是示例3的情况。
- 在进行头节点的处理使用while处理连续多个头节点值等于
val
的情况而不是if(if只能处理一次)(例如[1,1,1,2]
删除1
)。每次删除节点时记得更新head节点,并使用delete释放内存 -
如果
cur->next
的值等于val
,则修改cur->next
跳过该节点,并释放内存。如果不等,则正常移动到下一个节点。
方法二:虚拟头节点法(复杂度为O(n))
虚拟头节点法属于是对法一进行的一个优化操作,可以显著简化链表操作,尤其是在处理涉及头节点删除或修改的问题时。
对比法一的优点:
- 无需特殊处理头节点,虚拟头节点始终作为链表的“前置节点”,使得真正的头节点(
dummy->next
)和其他节点的删除逻辑完全一致,避免分支判断。 - 简化代码的结构,法一需要进行两步操作,需先对头节点进行预处理操作,再去处理其余节点。而使用虚拟头节点则只需要一个循环即可处理所有节点,避免代码冗余从而简化代码。
-
虚拟头节点确保
cur
初始指向一个非空节点(dummy
),因此cur->next
的访问总是安全的(无需额外判空)。
具体实现代码如下:
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;}
};
关键点说明:
- 虚拟头节点:创建一个虚拟头节点作为原链表的起始点,其next指针指向的为原链表的head节点 (作用:统一处理逻辑,避免单独进行头节点删除操作)
- 返回值:这里需要注意我们需要返回的值应为虚拟头节点的next,若原链表所有节点都已被删除,那么虚拟头节点的next会变为NULL,则无需处理。
其余与方法一相同!
至此算法已经是最优解!完结撒花!!!🌸🌸🌸
四、题目总结
方法一:原链表删除元素
此方法先对头节点进行处理,若头节点的值等于 val
,则连续删除头节点直至其值不等于 val
。之后遍历链表,对非头节点中值等于 val
的节点进行删除操作。该方法时间复杂度为 O(n),不过在处理头节点时需要额外的逻辑判断,且要分别处理头节点和非头节点的删除情况,代码相对复杂。
方法二:虚拟头节点法
这种方法创建了一个虚拟头节点,将其 next
指针指向原链表的头节点。通过遍历链表,检查每个节点的下一个节点,若其值等于 val
则进行删除。此方法的优点在于统一了头节点和其他节点的删除逻辑,避免了额外的分支判断,简化了代码结构。时间复杂度同样为 O(n),是对方法一的优化。
总结
在处理链表删除操作时,使用虚拟头节点能有效简化代码,避免因头节点的特殊情况而增加的复杂逻辑,提高代码的可读性和可维护性。两种方法的时间复杂度均为线性,已达到最优解。在实际编程中,要养成手动释放堆区内存的习惯,防止内存泄漏。今天的题解分享到这里,谢谢大家!!!荆轲刺秦!!!