• 链表经典题带刷(内含精华:链表深拷贝)


    目录

    链表的回文判断

    题目描述

    解题逻辑

    代码实现

    相交链表

    题目描述

    解题逻辑 

     代码实现

    环形链表判断

    题目描述

    解题逻辑

     代码实现

    环形链表进阶版

    题目描述

    解题逻辑

    丐版:

    升级版

    代码实现

    丐版

     升级版

    复制带随机指针的链表

    问题描述

    解题逻辑 

    平民版:

    贵族版:

    代码实现

    平民版:

    贵族版:

    链表的回文判断

    题目描述

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

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

    解题逻辑

    这道题有些大佬把链表中的数据拷贝到了数组上再处理,跑过了,当然,如果在面试敢那么写也是真的勇士。

    普通的解题思路就有些朴实无华且枯燥了:

    第一步:利用快慢指针得到中间指针的位置。

    我们可以看到,慢指针走一步,快指针走两步,最后得到中间指针。

    第二步:逆置中间指针之后的链表。

    这时候我们的重构已经完成了。

    第三步:比较两个新的链表的元素是否相同。

     从图中可以看出,从head1和head2开始,比较两个链表从头到尾的元素是否相同,如果相同就是回文。

    代码实现

    1. bool chkPalindrome(ListNode* A)
    2. {
    3. //排除0个和1个元素的情况
    4. if(A == NULL || A->next == NULL)
    5. return true;
    6. //第一步找到中间节点
    7. ListNode* fast = A;
    8. ListNode* slow = A;
    9. while(fast && fast->next)
    10. {
    11. slow = slow->next;
    12. fast = fast->next->next;
    13. }
    14. //第二步逆置后半段
    15. ListNode* prev = slow;
    16. ListNode* cur = slow->next;
    17. while(cur)
    18. {
    19. ListNode* back = cur->next;
    20. cur->next = prev;
    21. prev = cur;
    22. cur = back;
    23. }
    24. slow->next = NULL;
    25. //第三步,比较两个链表
    26. ListNode* cur1 = A;
    27. ListNode* cur2 = prev;
    28. while(cur1 && cur2)
    29. {
    30. if(cur1->val != cur2->val)
    31. {
    32. return false;
    33. }
    34. cur1 = cur1->next;
    35. cur2 = cur2->next;
    36. }
    37. return true;
    38. }

    相交链表

    题目描述

    给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

    如图所示,如果是相交链表,返回的应该是C节点的地址。

    解题逻辑 

    第一步:从headA和headB开始遍历,分别求出两个链表的长度,并且比较尾节点是否是同一个节点,如果是同一个则是交叉链表,如果尾节点不是同一个,则不构成交叉链表,返回NULL。

    第二步:两个指针都从头开始走,但是较长的链表的遍历指针先走两个长度的差值步(lengthA - lengthB的绝对值), 然后开始比较两个指针所指向的地址是否相同,一旦相同,则返回相应的地址。

     代码实现

    1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
    2. {
    3. //第一步
    4. if(headA == NULL || headB == NULL)
    5. {
    6. return NULL;
    7. }
    8. struct ListNode *cur_A = headA;
    9. struct ListNode *cur_B = headB;
    10. int length_A = 1;
    11. int length_B = 1;
    12. while(cur_A->next)
    13. {
    14. ++length_A;
    15. cur_A = cur_A->next;
    16. }
    17. while(cur_B->next)
    18. {
    19. ++length_B;
    20. cur_B = cur_B->next;
    21. }
    22. if(cur_A != cur_B)
    23. {
    24. return NULL;
    25. }
    26. //第二步
    27. struct ListNode * longest_head = headA;
    28. struct ListNode * shortest_head = headB;
    29. if(length_A < length_B)
    30. {
    31. longest_head = headB;
    32. shortest_head = headA;
    33. }
    34. for(int i = 0; i < abs(length_B - length_A); i++)
    35. {
    36. longest_head = longest_head->next;
    37. }
    38. while(longest_head != shortest_head)
    39. {
    40. longest_head = longest_head->next;
    41. shortest_head = shortest_head->next;
    42. }
    43. return longest_head;
    44. }

    环形链表判断

    题目描述

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

    如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

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

    如图所示,链表中尾节点的next链接到第二个节点,带环,返回true。

    解题逻辑

    这道题可以使用双指针法解决,一快一慢,快指针一次走两步,慢指针一次走一步,如果链表不带环,快指针会先走到空,自然返回false,如果在指针移动过程中,慢指针赶上了快指针,这说明链表带环。

     代码实现

    1. bool hasCycle(struct ListNode *head)
    2. {
    3. struct ListNode *fast = head;
    4. struct ListNode *slow = head;
    5. while(fast != NULL && fast->next != NULL)
    6. {
    7. fast = fast->next->next;
    8. slow = slow->next;
    9. if(slow == fast)
    10. {
    11. return true;//追上了
    12. }
    13. }
    14. return false;//走到头了
    15. }

    环形链表进阶版

    题目描述

    给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

    也就是在第一题判断一个链表是不是环形链表的基础上,还要找到链表尾链接到的节点的位置(从0开始计算)。

    解题逻辑

     这道题可以分为两步,第一步是判断这个链表到底是不是带环链表,如果不是,都不用进入第二步,直接返回-1,如果是带环链表,则进入下一步。

    让我们直接快进到快慢指针相遇的那一刻。

    第二步有两种解题思路,接下来我们分别讲解:

    丐版:

    我们把带环链表构造成交叉链表,以原来的头节点作为head1,相遇节点的后一个节点作为head2,这样我们就可以用第二题的方法来解决第二步了。

    但是,要知道第二题的解题方法还是有些繁琐的,在求交叉节点位置之前,还要求得两条链表的长度。

    升级版

    第二种的方法则用到了数学推导的方法,让我们来看看:

    我们设:头节点head到入环节点POS的步数为X,入环节点POS相遇节点meet的步数为l,绕环一圈要走的步数为loop;

    慢指针走过的步数为l + x;

    快指针走过的步数为l + x + loop。

    因为慢指针走一步,快指针走两步,所以 2 * (l + x) = l + x + loop;

    两边化简一下,得到 l + x = loop;

    再调整一下得到 l = loop - x。

    也就是说,两个指针,一个从头节点head出发,一个从相遇节点meet出发,走过l(l = loop - x)步,会在入环节点处相遇。

    代码实现

    丐版

    1. //找到交叉链表的交叉节点
    2. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
    3. {
    4. if(headA == NULL || headB == NULL)
    5. {
    6. return NULL;
    7. }
    8. struct ListNode *cur_A = headA;
    9. struct ListNode *cur_B = headB;
    10. int length_A = 1;
    11. int length_B = 1;
    12. while(cur_A->next)
    13. {
    14. ++length_A;
    15. cur_A = cur_A->next;
    16. }
    17. while(cur_B->next)
    18. {
    19. ++length_B;
    20. cur_B = cur_B->next;
    21. }
    22. if(cur_A != cur_B)
    23. {
    24. return NULL;
    25. }
    26. struct ListNode * longest_head = headA;
    27. struct ListNode * shortest_head = headB;
    28. if(length_A < length_B)
    29. {
    30. longest_head = headB;
    31. shortest_head = headA;
    32. }
    33. for(int i = 0; i < abs(length_B - length_A); i++)
    34. {
    35. longest_head = longest_head->next;
    36. }
    37. while(longest_head != shortest_head)
    38. {
    39. longest_head = longest_head->next;
    40. shortest_head = shortest_head->next;
    41. }
    42. return longest_head;
    43. }
    44. //返回带环链表开始入环的第一个节点
    45. struct ListNode *detectCycle(struct ListNode *head)
    46. {
    47. //第一步,判断是不是带环链表
    48. struct ListNode *fast = head;
    49. struct ListNode *slow = head;
    50. if(head == NULL || head->next == NULL)
    51. return NULL;
    52. while(fast && fast->next)
    53. {
    54. fast = fast->next->next;
    55. slow = slow->next;
    56. if(fast == NULL || fast->next == NULL)//走到尽头,不是带环链表
    57. {
    58. return NULL;
    59. }
    60. if(fast == slow)//快指针追上慢指针
    61. {
    62. break;
    63. }
    64. }
    65. //构建交叉链表
    66. struct ListNode * meet = slow;
    67. struct ListNode * head1 = head;
    68. struct ListNode * head2 = meet->next;
    69. meet->next = NULL;
    70. //获得入环节点,并把链表复原
    71. struct ListNode * ret = getIntersectionNode(head1, head2);
    72. meet->next = head2;
    73. return ret;
    74. }

    我们可以看到,把寻找交叉链表的交叉节点的步骤加上,多出来一大堆代码,显然这个方法不太方便。

     升级版

    1. struct ListNode *detectCycle(struct ListNode *head) {
    2. struct ListNode *fast = head;
    3. struct ListNode *slow = head;
    4. if(head == NULL || head->next == NULL)
    5. return NULL;
    6. while(fast && fast->next)
    7. {
    8. fast = fast->next->next;
    9. slow = slow->next;
    10. if(fast == NULL || fast->next == NULL)
    11. {
    12. return NULL;
    13. }
    14. if(fast == slow)
    15. {
    16. break;
    17. }
    18. }
    19. slow = head;
    20. while(slow != fast)
    21. {
    22. slow = slow->next;
    23. fast = fast->next;
    24. }
    25. return slow;
    26. }

    明显感觉到,第二种方法的代码简洁了许多,第一种的优势是思路比较简单,第二种方法要进行一定的推导,两种方法各有千秋,还是看自己如何取舍。

    复制带随机指针的链表

    问题描述

    给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

    构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

    例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

    返回复制链表的头节点。

     

    解题逻辑 

    这道题的解题思路也有两种:

    平民版:

    第一步,忽略random指针,构建一个其他指向关系已经内容与原指针相同的链表,这一步还是很容易实现的。

     接下来要捋清楚copy的链表的random指向何处,就要和原链表比较,从头开始遍历,指向的节点排第几位,就链接到相应节点。

    我们可以清楚地看到,prev1指针遍历链表,直到遇到NULL,prev1走一步,prev2也走一步,cur1指针遍历链表找到与prev1->random相同的节点时,cur2正好也到达pre2->randow应该指向的位置,prev2(newprev)和cur2(newcur)根据原链表的关系链接。

    贵族版:

     第二种方法要一些巧思,一般来说靠自己比较难想到。我们先在原链表的每个节点之后插入一个新的节点。

     

    第二步,创建两个指针,cur和newcur分别指向原链表和新链表的节点,遍历链表,当原链表的cur节点的random指向非空节点时,新链表的指针newcur指针的random指向原链表cur节点的random的next节点。

    最后一步,将链表复原。

    代码实现

    平民版:

    1. struct Node* copyRandomList(struct Node* head) {
    2. if(head == NULL)
    3. return NULL;
    4. //复制链表
    5. struct Node* cur = head;
    6. struct Node* newprev = NULL;
    7. struct Node* newcur = NULL;
    8. struct Node* newhead = NULL;
    9. while(cur)
    10. {
    11. newcur = (struct Node*)malloc(sizeof(struct Node));
    12. if(cur == head)
    13. {
    14. newhead = newcur;
    15. }
    16. newcur->val = cur->val;
    17. newcur->random = cur->random;
    18. newcur->next = cur->next;
    19. if(newprev != NULL)
    20. newprev->next = newcur;
    21. newprev = newcur;
    22. cur = cur->next;
    23. }
    24. //对照原链表建立random链接
    25. newprev = newhead;
    26. while(newprev)
    27. {
    28. cur = head;
    29. newcur = newhead;
    30. while(newprev->random != cur)
    31. {
    32. cur = cur->next;
    33. newcur = newcur->next;
    34. }
    35. newprev->random = newcur;
    36. newprev = newprev->next;
    37. }
    38. return newhead;
    39. }

    贵族版:

    1. struct Node* copyRandomList(struct Node* head) {
    2. if(head == NULL)
    3. {
    4. return NULL;
    5. }
    6. //在原链表之后插入节点
    7. struct Node* cur1 = head;
    8. struct Node* cur2 = NULL;
    9. while(cur1)
    10. {
    11. struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
    12. newnode->val = cur1->val;
    13. newnode->next = cur1->next;
    14. newnode->random = NULL;
    15. cur1->next = newnode;
    16. cur1 = cur1->next->next;
    17. }
    18. //建立新节点之间的联系
    19. cur1 = head;
    20. struct Node* newhead = head->next;
    21. while(cur1)
    22. {
    23. cur2 = cur1->next;
    24. if(cur1->random == NULL)
    25. {
    26. cur2->random == NULL;
    27. }
    28. else
    29. {
    30. cur2->random = cur1->random->next;
    31. }
    32. cur1 = cur1->next->next;
    33. }
    34. //拆分两个链表
    35. cur1 = head;
    36. while(cur1)
    37. {
    38. cur2 = cur1->next;
    39. cur1->next = cur2->next;
    40. if(cur2->next != NULL)
    41. {
    42. cur2->next = cur2->next->next;
    43. }
    44. cur1 = cur1->next;
    45. }
    46. return newhead;
    47. }

  • 相关阅读:
    AI篇-chatgpt基本用法(文心一言也适用)
    Android 如何添加自定义字体
    JAVA三元表达式详解
    数值分析 | 常见数据插值方法
    优雅的springboot参数校验(一)
    单链表的排序问题
    在Windows上安装Go语言开发包
    pgsql在navicate导出sql再去其他库导入时会遇到的小问题
    hyperf 十六 session
    DHCP服务
  • 原文地址:https://blog.csdn.net/JDSZGLLL/article/details/126105989