• 解剖—单链表相关OJ练习题


    目录

    一、移除链表元素

    二、找出链表的中间节点 

    三、合并两个有序链表

    四、反转链表

    五、求链表中倒数第k个结点

    六、链表分割

    七、链表的回文结构

    八、判断链表是否相交  

    九、判断链表中是否有环(一)

    十、 判断链表中是否有环(二)


    注:第六题和第七题牛客没有C环境,我在C++环境下用C语言写这道题(目前还没学C++,请大佬们理解一下,理解万岁!!) 

    一、移除链表元素

    203. 移除链表元素 - 力扣(LeetCode)

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

    示例 1:

    输入:head = [1, 2, 6, 3, 4, 5, 6], val = 6
    输出:[1, 2, 3, 4, 5]
    
    1. struct ListNode* removeElements(struct ListNode* head, int val) {
    2. struct ListNode* prev = NULL, * cur = head;
    3. while (cur) {
    4. if (cur->val == val) {
    5. if (prev) {
    6. prev->next = cur->next;
    7. free(cur);
    8. cur = prev->next;
    9. }
    10. else {
    11. cur = head->next;
    12. free(head);
    13. head = cur;
    14. }
    15. }
    16. else {
    17. prev = cur;
    18. cur = cur->next;
    19. }
    20. }
    21. return head;
    22. }

    思路:链表常规删除,注意删除头节点的情况。

    1、创建指向当前节点的前一个节点的指针prev=NULL(头节点的前一个节点为空),指针cur指向链表头节点。

    2、以当前节点cur不为空为循环执行条件,判断当前节点的值是否等于val,等于则进行删除,不等于则prev更新为当前节点cur,当前节点cur指向下一个节点。

    3、当前节点的值等于val时:

    • 如果值为 val 的节点不在头节点(prev 不为 NULL),将 prev 的 next 指针指向当前节点的下一个节点,然后释放当前节点并更新。
    • 如果值为 val 的节点在链表头节点(prev 为 NULL),则释放原来的头节点,将 head 更新为当前节点的下一个节点。

    下面是第二种方法,创建新的链表,把不删除的节点链接到新链表 :

    1. struct ListNode* removeElements(struct ListNode* head, int val) {
    2. struct ListNode* cur = head; // 当前遍历节点
    3. struct ListNode* newhead = NULL; // 新链表的头节点
    4. struct ListNode* tail = NULL; // 新链表的尾节点
    5. while (cur) {
    6. if (cur->val != val) { // 当前节点的值不等于要删除的值
    7. if (tail == NULL) { // 第一次插入
    8. newhead = tail = cur; // 设置新链表的头和尾
    9. }
    10. else {
    11. tail->next = cur; // 将当前节点连接到新链表的尾部
    12. tail = tail->next; // 更新新链表的尾节点
    13. }
    14. cur = cur->next; // 移动到下一个节点
    15. tail->next = NULL; // 断开新链表的尾节点连接
    16. }
    17. else {
    18. struct ListNode* del = cur; // 保存需要删除的节点
    19. cur = cur->next; // 移动到下一个节点
    20. free(del); // 释放需要删除的节点的内存
    21. }
    22. }
    23. return newhead; // 返回新链表的头节点,已移除所有值为 val 的节点
    24. }

    二、找出链表的中间节点 

    876. 链表的中间结点 - 力扣(LeetCode)

    • 给你单链表的头结点 head ,请你找出并返回链表的中间结点。

    • 如果有两个中间结点,则返回第二个中间结点。

    示例 1:

    输入:head = [1,2,3,4,5]
    输出:[3,4,5]
    解释:链表只有一个中间结点,值为 3 。

    1. struct ListNode* middleNode(struct ListNode* head) {
    2. struct ListNode* fast = head, * slow = head;
    3. while (fast && fast->next) {
    4. slow = slow->next;
    5. fast = fast->next->next;
    6. }
    7. return slow;
    8. }

     思路:快慢双指针

    1、采用速度为1步的slow和速度为2步的fast,这样设置二者速度比较合理。

    2、循环结束的条件需要考虑一下,分为链表节点个数为奇数和偶数,当fast走到最后的结果如下:

    3、 所以循环结束的条件为 fast && fast->next 二者有一个为空。

    三、合并两个有序链表

    21. 合并两个有序链表

    • 将两个升序链表合并为一个新的 升序 链表并返回。新
    • 链表是通过拼接给定的两个链表的所有节点组成的。 

    示例 1:

    输入:l1 = [1,2,4], l2 = [1,3,4]
    输出:[1,1,2,3,4,4]
    
    1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    2. if (list1 == NULL)
    3. return list2;
    4. if (list2 == NULL)
    5. return list1;
    6. struct ListNode* tail = NULL, * head = NULL;
    7. while (list1 && list2) {
    8. if (list1->val < list2->val) {
    9. if (tail == NULL) {
    10. head = tail = list1;
    11. }
    12. else {
    13. tail->next = list1;
    14. tail = tail->next;
    15. }
    16. list1 = list1->next;
    17. }
    18. else {
    19. if (tail == NULL) {
    20. head = tail = list2;
    21. }
    22. else {
    23. tail->next = list2;
    24. tail = tail->next;
    25. }
    26. list2 = list2->next;
    27. }
    28. }
    29. if (list1)
    30. tail->next = list1;
    31. if (list2)
    32. tail->next = list2;
    33. return head;
    34. }

    思路:创建新链表,较小的进行链接。

    好像跟顺序表的题相像合并两个有序数组

    不过,这次是单链表,所以我们可以创建新链表,从前向后将较小的链接到链表中,不需要从后先前了。

    1、首先判断是否有链表为空,一个链表为空,则直接返回合并后的结果:另一个不为空的链表,都为空则返回任意一个链表。

    2、创建新链表的尾节点tail和头节点head,初始化为空。

    3、比较 list1 和 list2 当前节点的值。

    • 如果 list1 的当前节点值小于 list2 的当前节点值,将 list1 的节点添加到合并后的链表中。
    • 如果 list2 的当前节点值小于等于 list1 的当前节点值,将 list2 的节点添加到合并后的链表中。

    4、在向合并链表中添加节点时,需要检查 tail 是否为 NULL,

    • 如果是,说明这是第一个节点,因此同时更新 head 和 tail。
    • 否则,只需将新节点连接到 tail 的 next 并将 tail 更新为新节点。

    5、每次添加节点后,对应的链表list需要向后移动一位。

    6、最终循环结束时,如果有链表不为空,证明该链表剩余节点的值均大于另一个链表的最大值,则将该链表其余的节点依次插入新链表。

    7、返回新链表头节点head。

    四、反转链表

    206. 反转链表

     给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

    示例 1:

    输入:head = [1,2,3,4,5]
    输出:[5,4,3,2,1]

    思路(一): 创建新链表,从前向后取出原链表的节点进行头插。

    1. struct ListNode* reverseList(struct ListNode* head) {
    2. struct ListNode* cur = head;
    3. struct ListNode* rhead = NULL;
    4. while (cur) {
    5. struct ListNode* next = cur->next;
    6. cur->next = rhead;
    7. rhead = cur;
    8. cur = next;
    9. }
    10. return rhead;
    11. }

    1. 开始时,定义两个指针 `cur` 和 `rhead`。

    • `cur` 指向原始链表的头部 `head`
    • `rhead` 用来构建反转后的链表,初始为空,表示反转后链表的尾部。

    2. 进入循环,这个循环将遍历整个链表并进行反转操作。循环的条件是 `cur` 不为 NULL,表示还有节点需要处理。

    3. 在循环内部,首先创建一个临时指针 `next` 来保存 `cur` 的下一个节点,因为在修改 `cur->next` 指针之前,需要先保存下一个节点的信息,否则会丢失对它的引用。

    4. 接着,将 `cur->next` 指向 `rhead`,这一步实际上是将 `cur` 的下一个节点指向反转后链表的头部,从而将 `cur` 从原链表中分离出来。

    5. 更新 `rhead` 为 `cur`,这一步将 `cur` 成为反转后链表的新头部。

    6. 更新 `cur` 为 `next`,这一步将 `cur` 移动到原链表中的下一个节点,准备处理下一个节点。

    7. 循环继续执行,直到 `cur` 变为 NULL,表示已经遍历完整个链表。此时,`rhead` 指向反转后链表的头部。

    8. 返回 `rhead`,它现在指向反转后的链表的头部,完成链表的反转。

     思路(二):调转链表方向(三指针)

    • n1 初始化为 NULL,表示反转后的链表的尾部。
    • n2 初始化为链表的头部节点 head
    • n3 初始化为 n2 的下一个节点,即 n2->next
    1. struct ListNode* reverseList(struct ListNode* head) {
    2. if (head == NULL)
    3. return NULL;
    4. struct ListNode* n1 = NULL;
    5. struct ListNode* n2 = head;
    6. struct ListNode* n3 = n2->next;
    7. while (n2) {
    8. n2->next = n1;
    9. n1 = n2;
    10. n2 = n3;
    11. if (n3)
    12. n3 = n3->next;
    13. }
    14. return n1;
    15. }

    1、进入循环,这个循环将遍历整个链表并反转节点。

    2、在循环内部,首先将 n2 的 next 指针指向 n1,这是为了将 n2 的指针方向反转,将其指向前一个节点 n1,而不是原来的下一个节点。

    3、接着更新 n1 和 n2,将它们分别向前移动一个节点。n1 移动到 n2 的位置,n2 移动到 n3 的位置。

    4、继续检查 n3 是否为 NULL。如果 n3 不为 NULL,将 n3 移动到它的下一个节点,以便在下一轮循环中使用。

    5、循环继续执行,直到 n2 为 NULL,表示已经遍历完整个链表。此时,n1 指向原链表的最后一个节点,它成为了反转后链表的头部。

    6、返回 n1,它现在指向反转后的链表的头部,完成了链表的反转。

    五、求链表中倒数第k个结点

    链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

    输入一个链表,输出该链表中倒数第k个结点。

    1. struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    2. struct ListNode* slow=pListHead,*fast=pListHead;
    3. while(k){
    4. if(fast==NULL)
    5. return 0;
    6. fast=fast->next;
    7. k--;
    8. }
    9. while(fast){
    10. slow=slow->next;
    11. fast=fast->next;
    12. }
    13. return slow;
    14. }

    思路: 快慢指针

    快慢指针的速度都为1步,快指针先走k步,然后慢指针开始与快指针同时走,快指针走到最后为空时,慢指针则为倒数第k个节点。

    1、通过while循环先让fast走k步,如果fast为空则无法求出倒数第k个节点(k大于链表长度)返回值为0。

    2、第二个while循环slow和fast开始同时向后走,fast为空时,slow指向的节点即为所求倒数第k个节点。

    六、链表分割

     链表分割_牛客题霸_牛客网 (nowcoder.com)

    现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

    1. class Partition {
    2. public:
    3. ListNode* partition(ListNode* pHead, int x) {
    4. struct ListNode* lesshead, * lesstail, * greaterhead, * greatertail;
    5. lesshead = lesstail = (struct ListNode*)malloc(sizeof(struct ListNode));
    6. greaterhead = greatertail = (struct ListNode*)malloc(sizeof(struct ListNode));
    7. struct ListNode* cur = pHead;
    8. while (cur) {
    9. if (cur->val < x) {
    10. lesstail->next = cur;
    11. lesstail = lesstail->next;
    12. }
    13. else {
    14. greatertail->next = cur;
    15. greatertail = greatertail->next;
    16. }
    17. cur = cur->next;
    18. }
    19. lesstail->next = greaterhead->next;
    20. greatertail->next = NULL;
    21. pHead = lesshead->next;
    22. free(lesshead);
    23. free(greaterhead);
    24. return pHead;
    25. }
    26. };

     思路:假设x=5,链表各节点值为如下图:

    1、创建变量* lesshead, * lesstail为小于x的链表的头节点和尾节点,

             * greaterhead, * greatertail 为大于x的链表的头节点和尾节点。

    •     为两个新链表头节点开辟空间,储存值val和指向下一个节点next。

    2、在循环中判断cur的值是否小于x,小于则将cur链接到less链表,不小于则链接到grearter链表。

    3、两个新链表需要链接合并成完整链表。

    4、greatertail->next = NULL;

    • 这一步一定不能少,否则可以会出现循环链表:

    七、链表的回文结构

    链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

    对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

    给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

    测试样例:

    • 1->2->2->1
    • 返回:true
      
    1. class PalindromeList {
    2. public:
    3. struct ListNode* middleNode(struct ListNode* head){
    4. struct ListNode* slow=head;
    5. struct ListNode* fast=head;
    6. while(fast&&fast->next){
    7. fast=fast->next->next;
    8. slow=slow->next;
    9. }
    10. return slow;
    11. }
    12. struct ListNode* reverseList(struct ListNode* head){
    13. struct ListNode* cur=head,*rhead=NULL;
    14. while(cur){
    15. struct ListNode* next=cur->next;
    16. cur->next=rhead;
    17. rhead=cur;
    18. cur=next;
    19. }
    20. return rhead;
    21. }
    22. bool chkPalindrome(ListNode* head) {
    23. struct ListNode* mid=middleNode(head);
    24. struct ListNode* rmid=reverseList(mid);
    25. while(rmid){
    26. if(rmid->val==head->val){
    27. rmid=rmid->next;
    28. head=head->next;
    29. }
    30. else{
    31. return false;
    32. }
    33. }
    34. return true;
    35. }
    36. };

    思路:从中间点开始逆置, 从头节点和中间节点开始分别往后进行比较。

    1、middleNode函数用于找到中间节点,具体讲解参考二、找出链表的中间节点 

    2、 reverseList函数用于逆置链表,具体讲解参考四、反转链表

    3、 chkPalindrome函数中首先指针mid指向中间节点,指针rmid指向从中间节点到最后逆置后的新的中间节点,循环遍历rmid,rmid与头结点head从前往后依次比较,如果有不相同的值,则返回false,否则返回true,rmid走到链表时(即rmid为空时),循环结束。

    八、判断链表是否相交  

    160. 相交链表

    给你两个单链表的头节点 headA 和 headB 

    • 请你找出并返回两个单链表相交的节点。
    • 如果两个链表不存在相交节点,返回 null 。

    图示两个链表在节点 c1 开始相交

    ​​

    • 题目数据 保证 整个链式结构中不存在环。
    • 注意,函数返回结果后,链表必须 保持其原始结构 。
    1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    2. struct ListNode * tailA=headA;
    3. struct ListNode * tailB=headB;
    4. int lenA=1,lenB=1;
    5. while(tailA->next){
    6. tailA=tailA->next;
    7. lenA++;
    8. }
    9. while(tailB->next){
    10. tailB=tailB->next;
    11. lenB++;
    12. }
    13. if(tailA!=tailB){
    14. return NULL;
    15. }
    16. int gap=abs(lenA-lenB);
    17. struct ListNode * longList=headA;
    18. struct ListNode * shortList=headB;
    19. if(lenA
    20. longList=headB;
    21. shortList=headA;
    22. }
    23. while(gap--){
    24. longList=longList->next;
    25. }
    26. while(longList!=shortList){
    27. longList=longList->next;
    28. shortList=shortList->next;
    29. }
    30. return longList;
    31. }

     思路:长的链表走相差长度步数,然后短的开始和长的同时走,节点地址相等则相交。

    1、定义变量 tailA 和 tailB 分别指向链表A和B的头节点,分别遍历对应链表获得链表长度lenA和lenB,因为while循环结束条件是tail->next为空,所以len的值初始化为1。

    ​​

    2、通过比较 tailA 和 tailB 是否相等来判断两个链表的尾节点是否一致。如果不相等,说明两个链表不可能有公共节点,直接返回 NULL。

     ​​

    3、使用abs函数求出链表A与B长度差值,差值赋值给变量gap。 

    4、 创建两个指针longlist和shortlist分别指向链表A和B的头节点,然后判断链表大小,如果lenA大于lenB,则 longList=headB,否则 shortList=headA。

    5、 longlist先向后走gap步,为了保持和shortlist同步遍历。

    ​​ 

    6、longlist和shortlist同时向后遍历,当二者相等时,停止遍历返回二者任意一个即为两个单链表相交的起始节点。

     ​​

    九、判断链表中是否有环(一)

    141. 环形链表

    给你一个链表的头节点 head ,判断链表中是否有环。

    注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

    • 如果链表中存在环 ,则返回 true 。 否则,返回 false 。

    • 示例 1:

      ​​

      输入:head = [3,2,0,-4], pos = 1
      输出:true
      解释:链表中有一个环,其尾部连接到第二个节点。

    1. bool hasCycle(struct ListNode *head) {
    2. struct ListNode *fast=head;
    3. struct ListNode *slow=head;
    4. while(fast&&fast->next){
    5. fast=fast->next->next;
    6. slow=slow->next;
    7. if(fast==slow)
    8. return true;
    9. }
    10. return false;
    11. }

    思路:快慢指针 

    ​​

    指针slow一次走一步,指针fast一次走两步,这样在链表中走,如果有环存在则二者一定会相遇,因为fast是slow速度的两倍。如果fast走到最后为空,证明该链表没有环存在。

    当fast步数

    十、 判断链表中是否有环(二)

    142. 环形链表 II

    给定一个链表的头节点  head 

    • 返回链表开始入环的第一个节点。 

    • 如果链表无环,则返回 null

    • 不允许修改 链表。

    • 示例 1:

    ​​

    输入:head = [3,2,0,-4], pos = 1
    输出:返回索引为 1 的链表节点
    解释:链表中有一个环,其尾部连接到第二个节点。

    1. struct ListNode* detectCycle(struct ListNode* head) {
    2. struct ListNode* fast = head;
    3. struct ListNode* slow = head;
    4. while (fast && fast->next) {
    5. slow = slow->next;
    6. fast = fast->next->next;
    7. if (fast == slow) {
    8. struct ListNode* meet = slow;
    9. while (head != meet) {
    10. head = head->next;
    11. meet = meet->next;
    12. }
    13. return meet;
    14. }
    15. }
    16. return NULL;
    17. }

    思路:快慢指针

    • slow一次走一步,fast一次走两步,他们都从头节点head位置开始移动。

    • 设head到入环节点的距离为L,假设第一次相遇位置为meet,入环节点到meet的距离为X,相遇位置到入环节点的距离为环的周长C减去X。

    ​​

    • slow走的路程:L+X

    • fast走的路程: L+X+n*C(slow进环前,fast走了n圈,比如:环很小则fast可能走了好几圈slow才进环)。

    • 因为fast的速度是slow的两倍,所以fast走的路程是slow的两倍,一圈之内fast一定追上slow!!

    • 由此可以得到以下关系式:2(L+X)=L+X+n*C

    • 化简得到:L=n*C-X 即 L=(n-1)C+C-X,

    • 这样就知道了 L=C-X,即一个指针从相遇点开始走,一个指针从头节点开始走,他们会在入环点相遇。

  • 相关阅读:
    记录一次mysql死锁
    Elasticsearch7教程(1) 创建文档
    计算机毕业设计(附源码)python游戏论坛设计
    寒假训练——第四周(素数|因数)
    【实践篇】Redis最强Java客户端(四)之Ression分布式集合使用指南
    爬虫中网络请求的那些事之urllib库
    SourceTree 这是一个无效的源路径/URL
    List基本使用(C++)
    PHP 5 SimpleXML 函数
    MYSQL逻辑架构与存储引擎
  • 原文地址:https://blog.csdn.net/m0_73800602/article/details/133906614