面试题 02.01. 移除重复节点

本文介绍了一种算法,用于从未排序链表中移除重复节点,仅保留首次出现的节点。通过遍历链表并比较每个节点值,若发现重复则跳过重复节点,最终返回处理后的链表头。此算法适用于链表长度和元素范围在[0,20000]内的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

面试题 02.01. 移除重复节点

https://leetcode-cn.com/problems/remove-duplicate-node-lcci/submissions/

难度简单58收藏分享切换为英文关注反馈

编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。

示例1:

 输入:[1, 2, 3, 3, 2, 1]
 输出:[1, 2, 3]

示例2:

 输入:[1, 1, 1, 1, 2]
 输出:[1, 2]

提示:

  1. 链表长度在[0, 20000]范围内。
  2. 链表元素在[0, 20000]范围内。

执行用时:592 ms, 在所有 JavaScript 提交中击败了12.62%的用户

内存消耗:43 MB, 在所有 JavaScript 提交中击败了86.05%的用户

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var removeDuplicateNodes = function (head) {
    var ListNodeTemp = head;
    var temp = head;
    while (temp != null) {
        ListNodeTemp = temp;
        while (ListNodeTemp.next != null) {
            if (temp.val == ListNodeTemp.next.val) {
                ListNodeTemp.next = ListNodeTemp.next.next;
            }
            else ListNodeTemp = ListNodeTemp.next;
        }
        temp = temp.next; //指向下一个
    }
    return head;
};

 

<think>嗯,用户让我用C语言解决一个单链表去重的问题。首先,我得仔细看看问题描述。题目要求去除单链表重复节点,保持原来的顺序。比如输入的例子,输出去重后的链表元素顺序要和原来的第一次出现顺序一致。那问题来了,如何高效地检查重复呢? 用户提供的样例输入输出显示,重复的元素只保留第一次出现的,后面的都去掉。比如第三个样例输入是4 1 3 6 1 5 1,输出是4->1->3->6->5,后面的两个1被去掉了。所以,我需要遍历链表,记录已经出现过的元素,遇到重复的就删除。 接下来,输入格式是两行,第一行是链表长度p,第二行是p个整数。输出格式需要用方括号包裹,每个元素前加->,比如[->21->17->15]。那输出的时候要注意格式,特别是开头的->,然后每个元素之间也是->。 首先,我需要设计数据结构。单链表的结构体应该包含一个整数值和一个指向下一个节点的指针。比如struct Node { int data; struct Node* next; };。然后,创建链表的函数,根据输入的整数数组生成链表。这一步应该没问题,逐个读取数字,创建节点,链接起来。 接下来是去重的逻辑。这里有两种常见方法:使用哈希表来记录已出现的元素,或者对于每个节点,检查其后的所有节点是否有重复。但C语言中实现哈希表可能比较麻烦,特别是考虑到时间和空间复杂度的话。如果链表长度很大,双重循环的时间复杂度会是O(n&sup2;),但对于题目中的样例,可能还是可以接受的,因为用户给的样例输入多是14个元素。不过,如果p很大的话,这种方法可能不够高效。但考虑到题目可能没有特别强调时间效率,可能先采用双重循环的方法。 具体来说,对于每个当前节点,检查其后续的节点,如果有重复的,就删除后面的重复节点。这样,外层循环遍历每个节点,内层循环检查后续节点并删除重复。这样能保证保留第一个出现的元素,后面的重复都被删除。 不过,这样处理的话,需要注意链表节点的删除操作,即需要维护前驱节点,以便正确修改next指针。例如,当当前节点是curr,然后遍历后面的节点,当发现某个节点的数据与curr的数据相同,就需要删除该节点,这时候需要记录该节点的前一个节点prev,将prev->next指向该节点的下一个,然后释放内存。 或者,另一种方法是使用一个数组或者哈希集合来记录已经出现过的元素。遍历链表时,如果当前元素已经在集合中,则删除当前节点,否则将元素加入集合。这种方法的时间复杂度是O(n),但需要额外的空间。由于题目中的元素是整数,如果数值范围不大,可以用数组标记,但如果数值范围很大,可能不太行。不过,用户输入的样例中数值都比较小,可能测试用例的数值范围有限。但考虑到通用性,可能还是需要动态的数据结构来记录已出现的元素。比如,可以用一个动态数组,每次遇到新元素就加入数组,然后检查后续节点是否在数组中存在。 但C语言中动态数组管理可能稍微麻烦,需要手动分配和扩展内存。或者可以使用一个简单的链表来保存已经存在的元素,但这样检查是否存在的时间复杂度又会变成O(n&sup2;),不过整体的时间复杂度还是O(n&sup2;),但可能实际运行更快一些,因为内层循环的链表可能较短。 权衡之下,可能还是采用双重循环的方法,虽然时间复杂度高,但实现起来简单,适合题目中的情况。 接下来,具体的步骤: 1. 读取输入,创建链表2. 遍历链表,对每个节点,检查后续节点是否有重复,如果有则删除。 3. 输出处理后的链表。 首先,创建链表部分。用户输入第一行是p,第二行是p个整数。可以用scanf读取。然后逐个创建节点,链接成链表。需要注意的是,可能用户输入的p为0的情况,这时候链表为空,输出应该是[]。 然后,去重逻辑。外层循环从head开始,每次取当前节点curr。然后内层循环从curr的下一个节点开始,用另一个指针prev_node和current_node。prev_node初始化为curr,current_node初始化为curr->next。然后遍历,如果current_node的数据等于curr的数据,则删除current_node,prev_node的next指向current_node的next,free(current_node),current_node更新为prev_node->next。否则,prev_node和current_node都后移一位。 这样,每个外层循环处理当前节点的后续所有重复节点,确保当前节点之后没有重复的数据。然后外层循环移动到下一个节点,直到curr->next为空。 例如,对于链表21->17->21->15->17: - curr指向第一个21,检查后面所有节点,发现第三个节点21,删除,链表变为21->17->15->17。然后继续检查,发现后一个17,也删除,变为21->17->15。 - curr移动到下一个节点17,检查后面是否有17,发现后一个节点被删除了,所以没有。 - curr移动到15,后面没有节点,结束。 这样就完成了去重。 然后输出部分。输出的格式是每个元素前加->,并且用[]包裹。比如,链表为空的话输出[]。如果有元素的话,开头是[->,然后每个元素用->连接,后是]。例如,样例输出中的[->21->17->15]。 在代码中,需要先处理链表为空的情况,否则遍历链表,拼接字符串。可以用动态字符串或者直接逐个打印。例如,先打印[,然后遍历链表,每个节点打印->%d,后打印]。 接下来,处理内存释放的问题。在删除节点的时候,需要及时free掉,避免内存泄漏。整个程序结束后,也需要释放整个链表的内存。 现在,编写代码的大体结构: 1. 定义链表节点结构体。 2. 读取输入,创建链表。 3. 去重处理。 4. 输出链表。 5. 释放链表内存。 可能需要注意的细节: - 输入可能包含多个重复数字,需要全部处理。 - 处理头节点的时候,可能需要特殊的处理,但在这个方法中,外层循环从head开始,每个curr处理其后所有的重复节点,所以头节点不会被跳过。 - 当链表为空时,直接输出[]。 - 输出时,每个元素前都有->,包括第一个元素,所以输出的格式是[->element1->element2...]。 现在,代码编写步骤: 结构体定义: struct Node { int data; struct Node* next; }; 创建链表的函数: struct Node* createLinkedList(int arr[], int n) { struct Node *head = NULL, *tail = NULL; for (int i = 0; i < n; i++) { struct Node* newNode = (struct Node*)malloc(sizeof(struct Node)); newNode->data = arr[i]; newNode->next = NULL; if (head == NULL) { head = newNode; tail = newNode; } else { tail->next = newNode; tail = newNode; } } return head; } 去重函数: void removeDuplicates(struct Node* head) { struct Node* curr = head; while (curr != NULL && curr->next != NULL) { struct Node* prev = curr; struct Node* nextNode = curr->next; while (nextNode != NULL) { if (nextNode->data == curr->data) { // 删除nextNode prev->next = nextNode->next; free(nextNode); nextNode = prev->next; } else { prev = nextNode; nextNode = nextNode->next; } } curr = curr->next; } } 这样,外层循环的每个curr节点,内层循环检查curr后面的所有节点,如果数据相同则删除。注意,这里的内层循环中,prev初始化为curr,然后nextNode是curr->next,每次比较nextNode的数据是否等于curr的数据,如果是,则删除nextNode,prev的next指向nextNode的next,并free。否则,prev和nextNode都向后移动。 这样处理之后,每个curr节点的后续节点都不会有重复的curr->data的值。 然后,输出函数: void printLinkedList(struct Node* head) { printf("["); struct Node* curr = head; while (curr != NULL) { printf("->%d", curr->data); curr = curr->next; } printf("]\n"); } 主函数部分: int main() { int p; scanf("%d", &p); int arr[p]; for (int i = 0; i < p; i++) { scanf("%d", &arr[i]); } struct Node* head = createLinkedList(arr, p); removeDuplicates(head); printLinkedList(head); // 释放链表内存 struct Node* curr = head; while (curr != NULL) { struct Node* temp = curr; curr = curr->next; free(temp); } return 0; } 但是这里有个问题,当p为0的时候,用户输入的第二行可能不存在,或者如何处理?根据题目描述,用户输入的第二行包含p个整数,所以当p=0时,可能没有第二行输入。但在实际代码中,如果p=0,读取第二行的输入可能会导致问题。因此,需要在代码中处理p=0的情况。 例如: 在main函数中,当p=0时,直接输出[],并且不需要读取第二行的输入。 修改主函数: int main() { int p; scanf("%d", &p); if (p == 0) { printf("[]\n"); return 0; } int arr[p]; for (int i = 0; i < p; i++) { scanf("%d", &arr[i]); } struct Node* head = createLinkedList(arr, p); removeDuplicates(head); printLinkedList(head); // 释放内存 struct Node* curr = head; while (curr != NULL) { struct Node* temp = curr; curr = curr->next; free(temp); } return 0; } 这样当p=0时,直接输出空链表,并跳过后续处理。 但需要考虑当p=0时,用户是否真的不会输入第二行。例如,如果用户输入p=0后,仍然输入了一行数字,但代码此时没有读取,可能会导致后面的输入被当作其他程序的输入。不过根据题目描述,输入是严格按照格式给出的,所以当p=0时,第二行不存在,所以代码是正确的。 接下来,测试样例是否符合。 比如第一个样例输入: 5 21 17 21 15 17 链表创建后是21->17->21->15->17。去重过程: curr是第一个21,检查后面的所有节点: - 下一个节点17,不等于21,继续。 - 下一个节点21,等于,删除。链表变为21->17->15->17。 - 继续检查下一个节点15,不等于。 - 下一个节点17,不等于。 curr移动到下一个节点17,检查后面的节点: - 下一个节点15,不等于。 - 下一个节点17,等于,删除。链表变为21->17->15。 curr移动到15,后面没有节点,结束。 输出结果为[->21->17->15],正确。 另一个样例输入14 2 8 1 7 2 6 2 1 1 3 2 1 10 11 去重后的输出是2->8->1->7->6->3->10->11。检查是否正确: 原链表中的元素顺序是2,8,1,7,2,6,2,1,1,3,2,1,10,11。去重时,保留每个元素的第一次出现,后面的重复都被删除: - 第一个2:后面所有的2都会被删除。 - 接下来8:后面没有重复。 - 1:后面重复1被删除。 - 7:无重复。 - 第二个2被删除(因为已经被前面的处理了)。 - 6:无重复。 - 第三个2被删除。 - 第二个1被删除。 - 第三个1被删除。 - 3:无重复。 - 第四个2被删除。 - 第四个1被删除。 - 10和11:无重复。 所以终的链表2->8->1->7->6->3->10->11,正确。 第三个样例输入: 7 4 1 3 6 1 5 1 去重后应该是4->1->3->6->5。检查: 原链表4->1->3->6->1->5->1。 处理: - 4:后面没有重复。 - 1:后面的两个1都会被删除。 - 3:无重复。 - 6:后面的1会被保留吗?因为处理到6的时候,curr是6,检查后面是否有6的数据,没有,所以后面的1不会被删除。但是原问题中,后面的1是否已经被前面的处理? 哦,原来去重的逻辑是,每个curr节点处理其后所有节点重复。所以,当处理第一个1的时候,后面所有的1都会被删除。例如,curr是第一个1,后面的节点是3->6->1->5->1,那么当处理到curr=1时,后面的1都会被删除。这样处理后,链表变为4->1->3->6->5。后的两个1已经被删除。 那第三个样例处理后的链表是正确的。 现在,代码是否能够正确处理这些情况? 在removeDuplicates函数中,每个curr节点处理其后所有节点,删除与curr->data相同的节点。这样,当处理第一个1的时候,后面所有1都会被删除,不管之后是否还有其他的节点。这样,后面的节点中的1都会被删除,所以第三个样例中的后面的三个1都会被删除,得到4->1->3->6->5,正确。 那代码是正确的吗? 看起来是的。现在需要测试边界情况,比如链表全为重复元素,例如输入3个5,应该输出[->5]。 或者输入链表长度为1,输出该元素。 另一个测试用例: 输入: 3 1 1 1 处理后链表应为[->1] 代码中,curr是第一个1,检查后面的节点,删除所有1链表只剩下头节点1,正确。 现在,可能的错误点在于,当删除节点时,链表指针的处理是否正确。 例如,在removeDuplicates函数中,外层循环的条件是curr != NULL && curr->next != NULL。当curr->next为NULL时,内层循环不会执行,直接curr移动到下一个节点,即curr->next(NULL),所以外层循环结束。这样处理是正确的。 另外,当链表为空时,主函数中的p=0的情况处理正确。 现在,内存释放是否正确?在main函数中,处理完链表后,遍历链表并逐个释放节点。假设链表已经被正确修改,所以遍历后的释放是正确的。 综上,代码应该可以正确解决问题。</think>以下是使用C语言解决单链表去重问题的完整实现方案,包含详细注释和边界条件处理: ```c #include <stdio.h> #include <stdlib.h> // 链表节点结构体 struct Node { int data; struct Node* next; }; // 创建链表函数 struct Node* createLinkedList(int arr[], int n) { if(n == 0) return NULL; struct Node *head = NULL, *tail = NULL; for(int i=0; i<n; i++) { struct Node* newNode = (struct Node*)malloc(sizeof(struct Node)); newNode->data = arr[i]; newNode->next = NULL; if(!head) head = tail = newNode; else { tail->next = newNode; tail = newNode; } } return head; } // 去重核心算法(时间复杂度O(n&sup2;)) void removeDuplicates(struct Node* head) { struct Node* current = head; while(current && current->next) { // 以current为基准,扫描后续所有节点 struct Node* checker = current; while(checker->next) { if(checker->next->data == current->data) { // 删除重复节点 struct Node* temp = checker->next; checker->next = temp->next; free(temp); } else { checker = checker->next; } } current = current->next; } } // 格式化输出函数 void printList(struct Node* head) { printf("["); for(struct Node* curr=head; curr; curr=curr->next) { printf("->%d", curr->data); } printf("]\n"); } // 内存释放函数 void freeList(struct Node* head) { while(head) { struct Node* temp = head; head = head->next; free(temp); } } int main() { int p; scanf("%d", &p); // 处理空链表特例 if(p == 0) { printf("[]\n"); return 0; } // 读取输入数据 int* arr = (int*)malloc(p * sizeof(int)); for(int i=0; i<p; i++) { scanf("%d", &arr[i]); } // 创建并处理链表 struct Node* head = createLinkedList(arr, p); removeDuplicates(head); printList(head); // 释放内存 freeList(head); free(arr); return 0; } ``` **代码解析:** 1. **数据结构设计:** - 使用`struct Node`定义链表节点,包含数据域和指针域 - 支持动态内存分配,适应任意长度的输入 2. **去重算法实现:** - 双指针扫描法:外层循环遍历所有节点,内层循环检查后续重复 - 时间复杂度:$O(n^2)$,空间复杂度:$O(1)$ - 优势:无需额外存储空间,适合内存受限场景 3. **关键处理细节:** ```c // 示例:删除重复节点的核心逻辑 if(checker->next->data == current->data) { struct Node* temp = checker->next; checker->next = temp->next; // 跳过重复节点 free(temp); // 释放内存 } ``` 4. **边界条件处理:** - 空链表输入:直接输出`[]` - 全重复数据:例如输入`[5,5,5]`输出`[->5]` - 首尾节点重复:例如输入`[1,2,3,1]`输出`[->1->2->3]` **测试用例验证:** ```text 输入: 5 21 17 21 15 17 处理过程: 1. 创建链表2117→2115→17 2. 删除第二个21后一个17 3. 输出:[->21->17->15] 输入14 2 8 1 7 2 6 2 1 1 3 2 1 10 11 处理过程: 1. 保留每个元素的首次出现位置 2. 终输出:[->2->8->1->7->6->3->10->11] ``` **进阶优化方向:** 1. 使用哈希表优化时间复杂度到$O(n)$ 2. 支持大规模数据(百万级节点)处理 3. 增加稳定性测试(连续重复/间隔重复混合场景) 建议通过LeetCode平台(面试题 02.01. 移除重复节点)进行更多测试用例验证。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值