Remove Duplicate from List(链表去重)

本文详细解析了链表去重问题,包括如何去除重复元素、仅保留单一元素,以及删除指定值的元素。通过实例代码,深入探讨了链表操作的关键技巧。
之前在[url=http://kickcode.iteye.com/blog/2263414]一篇文章里[/url]总结了一部分链表的题目,这里主要例举一下链表去重的问题。链表去重有很多种情况,有些要求我们只保留单独出现的元素,有些要求我们使重复的元素只能出现一次。对于可能会删除头结点的题目,我们一般采用新建一个helper节点,使helper指向head,进行操作,最后返回helper.next就可以了。下面是leetcode中有关链表去重的一些题目

[b]1,Remove Duplicates from Sorted List[/b]
给定一个链表,去除重复的元素,使每个元素最多出现一次。
例如:给定 1->1->2, 返回 1->2.
给定 1->1->2->3->3, 返回 1->2->3.

使用一个helper保留头结点,然后依次遍历链表。代码如下:

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null) return null;
ListNode helper = head;
while(head.next != null) {
if(head.val == head.next.val) {
head.next = head.next.next;
} else {
head = head.next;
}
}
return helper;
}
}


[b]2,Remove Duplicates from Sorted List II[/b]
给定一个链表,要求删除重复的元素,只保留单独的元素。
例如:给定 1->2->3->3->4->4->5, 返回 1->2->5.
给定 1->1->1->2->3, 返回 2->3.

因为头结点有可能被删除,此时我们用一个helper指向当前的头节点,进行遍历。代码如下:

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null) return head;
ListNode helper = new ListNode(0);
helper.next = head;
head = helper;
while(head.next != null && head.next.next != null) {
if(head.next.val == head.next.next.val){
int value = head.next.val;
while(head.next != null && head.next.val == value){
head.next = head.next.next;
}
} else {
head = head.next;
}
}
return helper.next;
}
}


3,Remove Linked List Elements
给定一个链表,一个目标元素target,删除链表中所有值为target的元素。
例如:给定: 1 --> 2 --> 6 --> 3 --> 4 --> 5 --> 6, target = 6
返回: 1 --> 2 --> 3 --> 4 --> 5

我们分析这道题,因为头结点也可能被删除,所以我们的创建一个helper节点,指向head节点。代码如下:

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode helper = new ListNode(0);
helper.next = head;
head = helper;
while(head.next != null) {
while(head.next != null && head.next.val == val) {
head.next = head.next.next;
}
if(head.next != null)
head = head.next;
else
break;
}
return helper.next;
}
}

[color=olive][i][b][url=http://kickcode.iteye.com/blog/2263414]链表总结[/url]这篇文章里介绍了在一个未排序的链表中删除节点,以及其它相似的问题,有兴趣的可以查看。[/b][/i][/color]
<think>嗯,用户想实现一个函数RemoveDuplicate,用于删除带头结点的非递减有序单链表中的复元素,并返回处理后的链表头指针。首先,我得理解这个问题。非递减有序单链表,也就是说链表中的元素是按非递减顺序排列的,可能有复的元素。目标是将所有复的元素删减到只保留一个,比如如果有1->2->2->3,处理后变成1->2->3。 然后,用户用的是C语言,所以需要处理指针和链表结构。带头结点的链表,头结点本身不存储数据,只是作为起始点。那函数需要遍历链表,比较当前节点和下一个节点的值,如果相同就删除复的节点。 首先,我需要考虑链表的遍历方式。因为链表是有序的,所以复元素都是相邻的。所以不需要考虑跳跃式的复,只需要逐个比较当前节点和下一个节点的值。这样时间复杂度应该是O(n),因为只需要一次遍历。 接下来,如何处理节点删除呢。假设当前节点是p,下一个节点是q。如果p->data等于q->data,那么需要将p的next指向q的next,然后释放q的内存。但这时候,可能还有多个复的节点,比如连续三个相同的值,所以在删除一个之后,还需要继续比较当前节点和新的下一个节点是否相同,直到不同为止。或者,可以一直比较下一个节点,直到下一个节点的值不同,然后一次性跳过所有复的节点。 比如,对于节点p,我们循环检查p->next是否存在,并且p->data等于p->next->data。如果相等,就删除p->next,直到p->next的值不同为止。这样就能保留一个节点,并删除后续的复节点。 需要注意的是,链表是带有头结点的,所以遍历应该从头结点的下一个节点开始,即头结点->next。然后使用一个指针current从头结点开始遍历,每次比较current的下一个节点和下下个节点的值。 比如,初始时current指向头结点。然后检查current->next和current->next->next是否存在。如果current->next的值等于current->next->next的值,那么就删除current->next->next,然后继续检查current->next的下一个节点是否还是相同的,直到不同为止。这时候,current不移动,继续处理可能剩下的复节点。或者,当current->next的值等于current->next->next的值时,删除current->next->next之后,current保持不动,继续检查current->next和新的current->next->next是否相同。直到没有复的,才移动current到下一个节点。 这样,具体的步骤可能是: 1. 检查链表是否为空或只有一个节点,如果是,直接返回头结点。 2. 初始化current指针指向头结点。 3. 循环遍历链表,直到current->next为NULL或者current->next->next为NULL。 4. 比较current->next和current->next->next的值。 5. 如果相等,则删除current->next->next节点,即保存该节点,将current->next的next指向该节点的next,然后释放该节点。 6. 复步骤4和5,直到current->next的next节点的值不同。 7. 当不再有复时,将current移动到current->next,继续遍历。 8. 最后返回头结点。 或者,另一种方法是使用两个指针,prev和current。prev初始化为头结点,current初始化为头结点的下一个节点。然后遍历链表,比较current和current->next的值。如果相同,就删除current->next,否则移动prev和current。但这种方法可能需要更多的指针操作。 不过,可能更高效的方式是,因为链表是有序的,当发现复时,一直删除后续的复节点,直到遇到不同的值。例如: current = head->next; while (current != NULL && current->next != NULL) { if (current->data == current->next->data) { // 删除current->next temp = current->next; current->next = temp->next; free(temp); } else { current = current->next; } } 但这个方法可能有问题。比如,当有三个连续相同的节点,例如1->2->2->2->3。第一次比较current(第二个节点)和current->next(第三个节点)相等,删除第三个节点,此时current->next变为第四个节点(原来的第四个节点是第二个2),此时current还是指向第二个节点,下一次循环继续比较current->next(第四个节点)是否等于current->data(2)。这样,第二个节点和第四个节点都是2,所以会再次删除。这样就能处理多个复的情况。是的,这个方法应该是可行的。但需要确保循环的条件正确。 不过,这种情况下,当删除current->next之后,current不需要移动,继续留在原地,这样下一次循环还会检查current和新的current->next是否相同。这样就能处理连续的多个复节点。例如: 假设链表是1 -> 2 -> 2 -> 2 -> 3 current指向第二个节点(第一个2)。比较current和current->next,发现相等,删除current->next,此时链表变成1->2->2->3。current还是指向第二个节点(第一个2)。再次循环,current->next是第二个2,比较发现相等,再次删除,此时链表变成1->2->3。这时候current->next是3,与current的data 2不同,于是current移动到3的位置。循环结束。 所以,这种方法能够正确删除所有复的节点,只保留一个。因此,这样的算法是可行的。 但需要注意,这种方法是否适用于头结点的情况。比如,头结点是头指针,头结点的下一个节点才是第一个数据节点。这时候,应该从头结点的下一个节点开始遍历吗? 例如,如果链表结构是:头结点 -> 1 -> 2 -> 2 -> 3,那么current应该初始化为头结点的下一个节点?或者是否应该从头结点开始遍历? 可能用户的问题中的链表结构是头结点不存储数据,头结点的next指向第一个数据节点。所以,正确的做法应该是遍历从第一个数据节点开始。也就是current = head->next。 但上述算法在current是第一个数据节点时,当current和current->next的值相等时,删除current->next,并保持current不动。这样就能处理连续复的情况。例如,当current指向第一个2,删除下一个2,current仍然指向第一个2,继续检查下一个是否是2。这样就能处理所有复的节点。 所以,这个算法的逻辑是正确的。 现在,编写代码的话,函数应该接收头结点指针,并返回头结点指针。因为头结点不会改变,所以返回头指针即可。 函数的大致结构: struct ListNode* RemoveDuplicate(struct ListNode* head) { if (head == NULL || head->next == NULL) { return head; } struct ListNode* current = head->next; while (current != NULL && current->next != NULL) { if (current->data == current->next->data) { struct ListNode* temp = current->next; current->next = temp->next; free(temp); } else { current = current->next; } } return head; } 但是,这样是否正确?例如,当有三个连续的复节点,是否会被正确处理? 比如,链表是1->2->2->2->3。current初始指向第一个2。第一次循环,current->next是第二个2,删除之后,current的next指向第三个2。此时current还是指向第一个2,继续循环。此时current->next是第三个2,再次删除,current的next指向3。这时候进入else,current移动到3。循环结束。这样就处理了所有的复节点。 这似乎是对的。但是,原题中的链表是带头结点的。所以在代码中,current应该初始化为head->next吗?例如,假设头结点是head,第一个数据节点是head->next。这时候,原代码中的current初始化为head->next是正确的。例如,如果链表的结构是头结点->1->2->2->3,那么head->next是第一个数据节点1。此时,current初始化为head->next,即1。然后比较current和current->next的值,即1和2,不同,进入else,current移动到2。此时,比较2和下一个2,相等,删除第二个2。current保持指向第一个2,继续循环,此时current->next是第三个2(原链表的第三个节点),再次删除。最后current的next指向3,进入else,current移动到3。循环结束。所以这种情况下是正确的。 但是,在头结点的情况下,头结点本身可能没有数据,所以遍历应该从头结点的下一个节点开始。所以,原代码中的current初始化为head->next是正确的。例如,当链表为空时,head->next是NULL,直接返回。当链表只有一个节点时,current是那个节点,current->next是NULL,循环不执行,返回正确。 综上,这样的代码应该可以正确处理问题。 但是,可能有一个问题,例如当链表是头结点 -> 2 -> 2 -> 2 -> NULL。此时,current初始化为第一个2。第一次循环,current->next是第二个2,删除之后,current->next变成第三个2。循环继续,再次删除,current->next变成NULL。此时循环条件current->next存在,所以继续循环。current的next是NULL,所以循环终止。最终链表变成头结点 -> 2 -> NULL。正确。 另一个测试用例:头结点->1->1->1->2->2->3。处理后应该是1->2->3。代码是否能正确执行? current初始化为第一个1。进入循环,current->next的值是1,等于current,删除current->next。current仍然指向第一个1,current->next现在变成第三个1。继续循环,再次比较,相等,删除。current->next变成2。此时进入else,current移动到2。比较current和current->next的值2,相等,删除current->next(下一个2)。current保持指向第一个2,current->next变成3。进入else,current移动到3。循环结束。最终链表是1->2->3。正确。 所以,这段代码是正确的。因此,正确的函数应该是这样的。 需要注意的点是: 1. 处理空链表或只有一个节点的链表的情况。 2. 正确遍历链表,并在发现复时删除后续节点,保持current不移动直到遇到不同的值。 3. 释放被删除节点的内存,避免内存泄漏。 现在,用户提供的引用中有关于forward_list的描述,其中提到forward_list是单链表,删除操作可能需要访问前一个节点。但在这个问题中,链表是带头结点的,所以可能更方便操作。例如,在遍历时,我们可能需要比较当前节点和下一个节点,而无需知道前一个节点,因为删除的是当前节点的下一个节点。这与引用中的情况类似,比如在单链表中,删除操作需要修改前一个节点的next指针。而在这个问题中,current指针实际上就是前一个节点的角色,因为current的next是需要比较的节点。例如,当current->next和current->next->next比较时,可能需要修改current->next的指针。或者,如代码中的处理,current指向的是当前要比较的节点,而删除的是current的下一个节点。比如,当current的data等于current->next的data时,删除current->next。此时,current本身不移动,直到没有复。 这样,代码的结构是正确的。 所以,最终的函数应该如上述代码所示,但需要确保头结点的处理正确。例如,函数接收的是头指针,处理的是头结点之后的节点。返回头指针即可。</think>以下是在C语言中实现`RemoveDuplicate`函数的逐步解决方案: ### 分析问题 在非递减有序单链表中,复元素必然连续出现。只需遍历链表,删除相邻复节点即可。时间复杂度为$O(n)$,空间复杂度$O(1)$[^1]。 ### 代码实现 ```c #include <stdlib.h> typedef struct ListNode { int data; struct ListNode *next; } ListNode; ListNode* RemoveDuplicate(ListNode *head) { if (head == NULL || head->next == NULL) { return head; } ListNode *current = head->next; // 从第一个数据节点开始遍历 while (current != NULL && current->next != NULL) { if (current->data == current->next->data) { ListNode *temp = current->next; current->next = temp->next; // 跳过复节点 free(temp); // 释放内存 } else { current = current->next; // 移动指针 } } return head; } ``` ### 关键步骤说明 1. **边界处理**:链表为空或仅头结点时直接返回 2. **双指针法**:通过`current`指针遍历链表,比较当前节点与下一节点的值 3. **删除操作**:当发现复时修改指针指向并释放内存,保持`current`不动以处理连续复 4. **内存管理**:显式释放被删除节点的内存避免泄漏 ### 示例演示 输入链表:`头结点 -> 1 -> 2 -> 2 -> 3 -> 3 -> 3` 输出链表:`头结点 -> 1 -> 2 -> 3` ### 复杂度分析 - **时间复杂度**:$O(n)$,仅需一次遍历 - **空间复杂度**:$O(1)$,仅使用固定数量指针
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值