链表的删除—做题总结 203. 移除链表元素

文章讲述了在C++和Java中优化链表删除操作的方法,涉及哑节点的构造、删除节点判断、内存管理,以及Java实现和递归解法的讨论。

一、最初的思路

设置了慢节点p1和快节点p2,遍历链表时p1紧跟p2,如果p2->val = val,那么通过 p1->next = p2->next; 把p2节点从节点中删除。

1、链表为空

直接返回head

 if(head == nullptr) return head;

2、删除的节点为头节点

头节点需要指向头结点的next
head = head->next;

注:这里可能会考虑到单节点的情况,[head]->null,如果head->val=val,那就要删除head,剩下的是null,head = head->next;是可以包括这种情况的

3、删除的节点不是头节点

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        //1、删除节点为空
        if(head == nullptr) return head;
        ListNode* p1 = nullptr;
        ListNode* p2 = head;
        ListNode* temp = nullptr;
        while(p2 != nullptr) {
            if(p2->val == val) {
                //2、删除的是头节点
                if(p1 == nullptr) {
                    head = head->next;
                    p2 = head;
                    /*因为要删除链表中《所有》值为val的节点,
                    所以要遍历完整个链表
                    这里的p2=head保证了后续链表的遍历*/
                }
                else { //3、删除的不是头节点
                    p1->next = p2->next;//把p2节点从链表删除
                    p2 = p1->next;
                    /*p2 = p1->next;保证遍历完整个链表*/
                }
            }
            else {
                temp = p2;
                p2 = p2->next;
                p1 = temp;
            }
        }
        return head;
    }
};

运行成功。
在这里插入图片描述

二、看了题解后的优化

看了题解发现可以简化写法。核心思想是增加一个哑节点。这样情况1(链表为空)情况2(删除的节点为头节点)都可以合并进 情况3中(删除的节点不是头节点)。

需要注意的点

(1)链表中节点的构造
力扣官方对单链表的定义:

  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) {}
  };

由此,创造一个节点可以写成:

struct ListNode* dummyHead = new ListNode(0, head);

(2)上文中的情况三“删除的节点不是头节点”,通过快慢指针删除对应的节点,可以通过一个指针temp完成

   struct ListNode* temp;
 temp->next = temp->next->next;

代码如下:

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        struct ListNode* fhead = new ListNode(0,head);//1
        struct ListNode* temp = fhead;
        //2思考这里为什么要用temp->next 而不是temp
        //尾节点 头节点
        while(temp->next != NULL) {
            if(temp->next->val == val) {
                temp->next = temp->next->next;
            }
            else {//3
                temp = temp->next;
            }
        }
        return fhead->next;//4
    }
};

犯了很多错误:

(1)哑节点的构造

题目的构造体没有用 typedef,不可以省略关键字 struct

struct ListNode* fhead = new ListNode(0,head);

//这样是错的,因为构造体并没有用typedef
ListNode* fhead = new ListNode(0,head);

(2)构造了哑节点 fhead 后的返回值

构造了哑节点 fhead 之后,注意返回值是 fhead->next,fhead 本身是不属于单链表的。

return fhead->next;

(3)temp->next 和 temp

为什么要比较 temp->next 节点不能用 temp 节点呢?
删除节点的关键步骤为 temp->next = temp->next->next; (判断的是 temp->next 这个节点)。本质上其实是用temp代替p1,temp->next代替 p2。

但如果判断变成temp这个节点(也就是假如判断的条件是if(temp->val = val;) ),要删除temp节点的话,是需要temp的上一个节点的,这里只用了temp遍历,是找不到上一个节点的。

为什么 while 语句中的判断条件是temp->next = NULL
遍历结束的条件是,temp->next = NULL;

(4)nullptr 和 NULL的区别。

接上一条,while(temp->next != NULL) 和 while(temp->next != nullptr) 的区别是什么?

NULL属于C 语言中的宏,后来 C++11 引入了 nullptr 关键字,都用来表示空指针。
在 C++ 中表示指针的地方,使用 nullptr 表示空指针。

https://blog.youkuaiyun.com/jiey0407/article/details/125383209?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169960180916800182157513%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=169960180916800182157513&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-125383209-null-null.142v96pc_search_result_base6&utm_term=nullptr%20%E5%92%8C%20NULL&spm=1018.2226.3001.4187

(5)while循环里面,如果写成如下形式 是错的:

         while(temp->next != NULL) {
            if(temp->next->val == val) {
                temp->next = temp->next->next;
            }
            temp = temp->next;
         }

会报错:
Line 19: Char 21: runtime error: member access within null pointer of type ‘struct ListNode’ (solution.cpp)
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:28:21

在这里插入图片描述
为什么一定需要那个else?
代码是通过判断 temp->next 这个节点的值是否为 val 进行删除操作的。

如图:假设删除了temp->next(a2),之后,a3会变成新的temp->next节点,所以要对a3进行判断。
在这里插入图片描述
因此在进行了删除操作 temp->next = temp->next->next; 之后,temp的位置是不能变化的

如果不进行删除操作,代表此时的 temp->next->val != val ,就需要把 temp往后移 temp = temp->next;

因此 else是必须要的

三、释放内存

C++编写的代码都需要释放内存,也就是上述所有代码其实都不完全正确,但是在力扣中是可以通过的。一定要注意释放内存。
delete temp;

四、Java实现

改于2023.12.11,之前是用C++写的,最近在学Java,用Java重写了一次。

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //链表删除
        //空链表
        //非空链表 添加头节点
        if(head == null) return null;

        ListNode myheah = new ListNode(-1,head);
        //这里需不需要一个临时变量呢?
        ListNode myh = myheah;
        while(myh.next != null) {//
            if(myh.next.val == val){
                myh.next = myh.next.next;
            } else {
                myh = myh.next;
            }
        }

        return myheah.next;
    }
}

分析:
注意这里是需要临时变量的。
new ListNode(-1,head) new在堆中开辟了一个空间,开辟空间的同时分配了一个地址,存在 myheah 引用中。如果遍历的时候用 myheah 这个变量,那while循环之后,链表的头节点地址是不清楚的。

四、递归解法

(还未完成)

<think>好的,用户现在想用C语言实现删除链表中的重复元素。之前我已经给过他们一个双指针法的解决方案,但现在他们可能希望看到不同的方法或者更优化的版本。根据用户提供的三个引用,都是关于LeetCode题目的解法,分别是删除排序链表中的重复元素I和II,以及用户之前的回答。需要综合这些信息,给出一个更全面的解答。 首先,我需要确认用户的需求。他们可能有一个链表,可能是有序的也可能是无序的,需要删除重复的元素。之前的回答是针对无序链表的,使用双指针法,时间复杂度O(n²)。而引用中的解决方案针对的是已排序的链表,可以更高效,比如O(n)的时间复杂度。因此,我需要区分这两种情况,分别给出解决方案。 用户可能没有明确说明链表是否排序,但引用的资料都是处理已排序链表的。因此,我应该先处理排序链表的情况,再提到无序链表的处理方法。这样能够覆盖更多的可能性,帮助用户理解不同场景下的解决方案。 接下来,我需要分析每个引用的代码。引用[1]是LeetCode 83题,删除排序链表中的重复元素,保留重复元素的第一个。例如,1->1->2变为1->2。引用[2]是LeetCode 82题,删除所有重复元素,比如1->1->2变为2。引用[3]似乎是一个不太标准的实现,可能有问题,比如使用了flag变量,但可能效率不高或者有错误。 用户之前的问题是关于删除重复数据,可能包括所有重复的节点,或者保留一个。需要明确用户的需求,但用户现在的问题比较广泛,所以需要分别说明两种情况:保留一次和全部删除。 因此,我应该分两个部分来回答:处理已排序链表(保留一次和删除全部重复),以及处理未排序链表的方法。这样用户可以根据实际情况选择合适的方案。 此外,用户可能关心代码的正确性、时间复杂度和空间复杂度。需要解释每种方法的特点,并给出示例代码,同时注意内存管理,避免内存泄漏。 最后,需要确保代码符合C语言的规范,比如结构体定义、指针操作、内存释放等。并且,在回答中引用提供的参考资料,比如引用[1]和[2],指出它们的应用场景。 可能还需要比较不同方法的优缺点,例如对于排序链表,O(n)的方法更高效;而无序链表则需要O(n²)时间或者使用哈希表(但C语言中实现哈希表可能较复杂)。 总结来说,回答应该包括: 1. 处理已排序链表的两种情况:保留一个或删除所有重复。 2. 处理未排序链表的方法,如双指针法或哈希表。 3. 代码示例和详细解释,包括内存管理。 4. 时间复杂度和适用场景的比较。 5. 相关问题,引导用户深入思考。 需要确保回答结构清晰,代码正确,并且覆盖用户可能的需求。同时,参考用户提供的引用内容,整合到回答中,以增强权威性和全面性。</think>### C语言实现链表去重的三种典型方法 #### 一、有序链表处理(保留重复元素一次) 当链表**已排序**时,可采用线性扫描法高效去重。该方法时间复杂度为 $O(n)$,适用于LeetCode 83题场景[^1]。 **实现原理**: 1. 使用单指针 `cur` 遍历链表 2. 比较当前节点与下一节点的值 3. 发现重复时跳过下一节点并释放内存 ```c struct ListNode* deleteDuplicates(struct ListNode* head) { if (!head) return head; struct ListNode* cur = head; while (cur->next) { if (cur->val == cur->next->val) { struct ListNode* temp = cur->next; cur->next = temp->next; free(temp); } else { cur = cur->next; } } return head; } ``` #### 二、有序链表处理(删除所有重复元素) 当需要**完全移除重复元素**时(LeetCode 82题场景[^2]),需增加前驱指针处理连续重复: ```c struct ListNode* deleteDuplicates(struct ListNode* head) { if (!head || !head->next) return head; struct ListNode dummy; dummy.next = head; struct ListNode* prev = &dummy; struct ListNode* cur = head; while (cur) { bool isDup = false; while (cur->next && cur->val == cur->next->val) { isDup = true; struct ListNode* temp = cur->next; cur->next = temp->next; free(temp); } if (isDup) { prev->next = cur->next; free(cur); cur = prev->next; } else { prev = cur; cur = cur->next; } } return dummy.next; } ``` #### 三、无序链表处理(通用解法) 当链表**未排序**时,可采用双指针法实现 $O(n^2)$ 时间复杂度去重[^4]: ```c void removeDuplicates(struct ListNode* head) { struct ListNode* ptr1 = head; while (ptr1 && ptr1->next) { struct ListNode* ptr2 = ptr1; while (ptr2->next) { if (ptr1->val == ptr2->next->val) { struct ListNode* temp = ptr2->next; ptr2->next = temp->next; free(temp); } else { ptr2 = ptr2->next; } } ptr1 = ptr1->next; } } ``` #### 方法对比 | 方法类型 | 时间复杂度 | 空间复杂度 | 适用场景 | |----------------|------------|------------|------------------------| | 有序保留单元素 | $O(n)$ | $O(1)$ | 已排序链表[^1] | | 有序完全去重 | $O(n)$ | $O(1)$ | 需彻底删除重复[^2] | | 无序双指针法 | $O(n^2)$ | $O(1)$ | 未排序小规模数据[^4] | | 哈希表法 | $O(n)$ | $O(n)$ | 未排序大规模数据(需额外实现哈希表)| #### 内存管理要点 1. 使用 `free()` 及时释放被删除节点 2. 修改指针指向时避免出现野指针 3. 对头节点特殊处理时建议使用哑节点 --相关问题-- 1. 如何用哈希表优化无序链表的去重效率? 2. 循环链表与双向链表的去重方法有何不同? 3.链表节点存储结构体等复杂数据类型时,去重逻辑需要做哪些调整? [^1]: LeetCode 83题标准解法,适用于保留单个重复元素 [^2]: LeetCode 82题进阶解法,实现完全去重 [^4]: 经典双指针法,通用但效率较低
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值