当前位置:   article > 正文

【数据结构】链表进阶_如何调用链表两次

如何调用链表两次

目录

链表的分类

链表种类的基本概述

带头双向循环列表程序的实现

带头双向循环列表的基本概述

List.h

List.c

Test.c

链表oj练习题

反转链表

链表的中间节点 

合并两个有序链表

环形链表I

环形链表II


链表的分类

链表种类的基本概述

链表可以由上图所示,组合成八种不同结构的链表类型

双向链表

 带头链表

 循环链表

带头双向循环列表程序的实现

带头双向循环列表的基本概述

带头:指针直接指向一个哨兵位的头节点       

好处:增删查改操作中,不需要改变头节点的值,所以不需要使用二级指针

双向:在每一个节点中,存放了两个指针,一个指向前一个节点,另一个指向后一个节点

好处:在插入删除操作中,不要再次遍历链表,降低时间复杂度

循环:链表的最后一个节点的next指针指向头节点,第一个节点的prev指针指向的是尾节点

好处:不需要通过遍历的手段寻找尾节点了。

List.h

结构体是链表的最小单元,在带头双向循环列表中,定义一个结构体,主要包含三个部分:存储的数据,前一个节点的指针,后一个节点的指针。

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. typedef int LTDataType;
  6. typedef struct ListNode ListNode;
  7. struct ListNode
  8. {
  9. ListNode* prev;
  10. ListNode* next;
  11. LTDataType data;
  12. };
  13. void ListInit(ListNode** phead);
  14. void ListDestory(ListNode* phead);
  15. void ListPushBack(ListNode* phead, LTDataType x);
  16. void ListPushFront(ListNode* phead, LTDataType x);
  17. void ListPopBack(ListNode* phead);
  18. void ListPopFront(ListNode* phead);
  19. void ListPrint(ListNode* phead);
  20. ListNode* ListFind(ListNode* phead, LTDataType x);
  21. void ListInsert(ListNode* phead, ListNode* pos, LTDataType x);
  22. void ListErase(ListNode* phead, ListNode* pos);
  23. struct ListNode* middleNode(struct ListNode* head);

List.c

各个函数的实现

ListInit

因为是带头的双向循环链表,先新建一个节点,再将prev和nex分别指向自己

  1. void ListInit(ListNode* phead)
  2. {
  3. //建立一个头节点
  4. //prev 和 next均指向phead
  5. //注意:这里改变了phead,也就是实参的值,所以需要二级指针
  6. phead = BuyListNode(0);
  7. phead->next = phead;
  8. phead->prev = phead;
  9. }

 这里我们运行程序,会发现

在plist中,仍然是<无法读取内存>,原因很简单,在该函数内,我们又试图改变传入的参数phead的值,但是形参无法改变实参,所以在这我们还需要使用二级指针。

  1. void ListInit(ListNode** pphead)
  2. {
  3. //建立一个头节点
  4. //prev 和 next均指向phead
  5. //注意:这里改变了phead,也就是实参的值,所以需要二级指针
  6. *pphead = BuyListNode(0);
  7. (* pphead)->next = *pphead;
  8. (* pphead)->prev = *pphead;
  9. }

修改后我们可以十分清晰地看到初始化后链表的物理结构

 plist,也就是我们链表的头指针,其中prev和next又分别指向了自己

ListPushBack&ListPushFront

单链表中的尾插:需要分两种情况,当单链表为空时,需要将newnode赋给头节点;当单链表中有节点时,需要找到链表中最后一个节点,将newnode与其链接

双向循环链表:第一个节点的prev指向的就是最后一个节点!

所以可以非常轻松的找到最后一个节点

同样地:头插就更加简单,phead的下一个节点就是第一个节点(能够存储有效数字的第一个节点)

由此我们可以看出双向链表的优势所在。

  1. void ListPushBack(ListNode* phead, LTDataType x)
  2. {
  3. //断言:如果phead=NULL,则报错
  4. assert(phead);
  5. ListNode* newnode = BuyListNode(x);
  6. //找到链表中最后一个节点
  7. ListNode* tail = phead->prev;
  8. //将newnode和头尾相连
  9. newnode->prev = tail;
  10. tail->next = newnode;
  11. newnode->next = phead;
  12. phead->prev = newnode;
  13. }
  14. void ListPushFront(ListNode* phead, LTDataType x)
  15. {
  16. assert(phead);
  17. ListNode* newnode = BuyListNode(x);
  18. ListNode* first = phead->next;
  19. newnode->next = first;
  20. first->prev = newnode;
  21. phead->next = newnode;
  22. newnode->prev = phead;
  23. }

ListPopback&ListPopFront

头删尾删与上面操作基本一致,需要注意的是,是否链表中只含有带有哨兵位的头节点了,所以需要断言判断。不要将链表整个的结构全部删除了。

  1. void ListPopBack(ListNode* phead)
  2. {
  3. assert(phead);
  4. assert(phead->next != phead);
  5. ListNode* last = phead->prev;
  6. ListNode* last_but_two = last->prev;
  7. last_but_two->next = phead;
  8. phead->prev = last_but_two;
  9. free(last);
  10. last = NULL;
  11. }
  12. void ListPopFront(ListNode* phead)
  13. {
  14. assert(phead);
  15. //检查传入的链表是否已经初始化
  16. assert(phead->next != phead);
  17. //检查链表是否只有一个哨兵位的节点
  18. ListNode* first = phead->next;
  19. ListNode* second = first->next;
  20. phead->next = second;
  21. second->prev = phead;
  22. free(first);
  23. first = NULL;
  24. }

ListInsert

  1. void ListInsert(ListNode* phead, ListNode* pos, LTDataType x)
  2. {
  3. assert(phead);
  4. ListNode* newnode = BuyListNode(x);
  5. ListNode* cur = pos->prev;
  6. cur->next = newnode;
  7. newnode->prev = cur;
  8. newnode->next = pos;
  9. pos->prev = newnode;
  10. }

ListErase 

  1. void ListErase(ListNode* phead, ListNode* pos)
  2. {
  3. assert(phead);
  4. ListNode* front = pos->prev;
  5. ListNode* back = pos->next;
  6. front->next = back;
  7. back->prev = front;
  8. free(pos);
  9. pos = NULL;
  10. }

ListPrint 

  1. void ListPrint(ListNode* phead)
  2. {
  3. ListNode* cur = phead->next;
  4. while (cur != phead)
  5. {
  6. printf("%d ", cur->data);
  7. cur = cur->next;
  8. }
  9. printf("\n");
  10. }

 ListFind

  1. ListNode* ListFind(ListNode* phead, LTDataType x)
  2. {
  3. assert(phead);
  4. ListNode* cur = phead->next;
  5. while (cur != phead)
  6. {
  7. if (cur->data == x)
  8. {
  9. return cur;
  10. }
  11. cur = cur->next;
  12. }
  13. return NULL;
  14. }

ListDesStory

 销毁链表的操作时,我们由之前的知识可知,链表是的最小单元是一个结构体,而结构体就是我们malloc而来的,所以我们需要先将所有节点销毁,再将整个结构,也就是头节点销毁。

值得注意的是,在遍历的过程中,free(cur)后,则会丢失对cur节点后的链表,所以需要先将cur->next进行保存,再将cur释放。

  1. void ListErase(ListNode* phead, ListNode* pos)
  2. {
  3. assert(phead);
  4. ListNode* front = pos->prev;
  5. ListNode* back = pos->next;
  6. front->next = back;
  7. back->prev = front;
  8. free(pos);
  9. pos = NULL;
  10. }

Test.c

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include"List.h"
  3. void TestList1()
  4. {
  5. ListNode* plist = NULL;
  6. ListInit(&plist);
  7. ListPushBack(plist, 1);
  8. ListPushBack(plist, 2);
  9. ListPushBack(plist, 3);
  10. ListPushBack(plist, 4);
  11. ListPushFront(plist, 0);
  12. ListPrint(plist);
  13. ListPopFront(plist);
  14. ListPrint(plist);
  15. ListPopBack(plist);
  16. ListPrint(plist);
  17. //ListDestory(plist);
  18. ListNode* pos = ListFind(plist,3);
  19. if (pos == NULL)
  20. {
  21. printf("查无此值\n");
  22. }
  23. else
  24. {
  25. printf("%p\n", pos);
  26. }
  27. pos = ListFind(plist, 3);
  28. if (pos == NULL)
  29. {
  30. printf("插入失败\n");
  31. }
  32. else
  33. {
  34. ListInsert(plist, pos, 10);
  35. }
  36. ListPrint(plist);
  37. pos = ListFind(plist, 3);
  38. if (pos == NULL)
  39. {
  40. printf("删除失败\n");
  41. }
  42. else
  43. {
  44. ListErase(plist, pos);
  45. }
  46. ListPrint(plist);
  47. ListDestory(plist);
  48. }
  49. int main()
  50. {
  51. TestList1();
  52. return 0;
  53. }

这里给出的是一个测试函数,在我们写程序的同时,我们应该写一点测一点,避免出现错误多到溢出的情况。

也可以写一个主菜单,实现链表的多个功能。

详情可参考

10.27单链表_zhangyuaizhuzhu的博客-CSDN博客

链表oj练习题

反转链表

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


 

提示:

链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/reverse-linked-list

 思路一:翻指针

注意!!!数据结构题目合理利用图表可以帮助我们更快的找到思路,解决问题

一般来说,解决一个需要重复完成的问题,通常需要三个步骤:

1.初始条件 2.迭代条件 3.结束条件

对于本道题而言,要实现链表的反转,自然而然地会想到双向链表,但是题目仅仅给了我们单链表,所以我们不难想到,可以倒转指针的指向,返回最后一个节点的指针。

 1.初始条件:我们定义三个结构体指针n1,n2,n3(保存n2后面的链表),其中n1指向空,n2指向第一个节点,n3指向第二个节点 

2.迭代过程:将原本n2指向n3的指针反转,指向n1。(将原本指向后一个节点的指针指向前一个节点)这步操作的目的是将整个链表翻转。

再将n1,n2,n3整体向后移位。这步操作的目的是进行迭代

一个循环体的基本操作有

1.改变指针指向

2.移位

每次循环,都将会进行一次完完整整的循环体

如此反复

 3.结束条件:由图可知,当n2指向NULL时,已经达到翻转的效果,所以,结束的条件为:

n2 ==NULL

  1. struct ListNode* reverseList(struct ListNode* head){
  2. //head为空的特殊条件
  3. if (head == NULL)
  4. {
  5. return head;
  6. }
  7. //初始条件
  8. struct ListNode* n1 = NULL;
  9. struct ListNode* n2 = head;
  10. struct ListNode* n3 = head->next;
  11. //结束条件
  12. while (n2 != NULL)
  13. {
  14. //迭代过程
  15. n2->next = n1;
  16. n1 = n2;
  17. n2 = n3;
  18. //n3有可能为空,则空指针的next会出问题
  19. if (n3 == NULL)
  20. {
  21. n3 = NULL;
  22. }
  23. else
  24. {
  25. n3 = n3->next;
  26. }
  27. }
  28. return n1;
  29. }

思路二:头插法

新建一个链表newhead,将head从前往后遍历,将head中的节点依次头插到newhead中

1.初始条件:cur = head(用于遍历原链表) newhead(新链表)next(保存cur后面的节点)

2.迭代过程完成我们想要的操作后,要将其他变量还原为开始迭代前的状态

将cur指向的节点头插入newhead中——我们想要的操作

newhead向前移动位置——还原操作

将cur和next向后移动位置——还原操作

3.结束条件:cur为空时即结束

  1. struct ListNode* reverseList(struct ListNode* head){
  2. if (head == NULL)
  3. {
  4. return NULL;
  5. }
  6. struct ListNode* newhead = NULL;
  7. struct ListNode* cur = head;
  8. struct ListNode* next = cur->next;
  9. while (cur != NULL)
  10. {
  11. cur->next = newhead;
  12. newhead = cur;
  13. cur = next;
  14. if (next)
  15. {
  16. next = next->next;
  17. }
  18. }
  19. return newhead;
  20. }

链表的中间节点 

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

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

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
 

提示:

给定链表的结点数介于 1 和 100 之间。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/middle-of-the-linked-list

方法一:遍历两次链表

  1. struct ListNode* middleNode(struct ListNode* head){
  2. struct ListNode* cur = head;
  3. int count = 1;
  4. while (cur->next != NULL)
  5. {
  6. cur = cur->next;
  7. count++;
  8. }
  9. cur = head;
  10. int i = 1;
  11. while (i < count / 2 + 1)
  12. {
  13. cur = cur->next;
  14. i++;
  15. }
  16. return cur;
  17. }

遍历第一次,求得链表的长度

遍历第二次找到中间节点

方法二:只遍历一次链表,快慢指针法

核心:慢指针走一步,快指针走两步

奇数偶数节点要分情况考虑

当有奇数个节点时

slow
fast
12345NULL

此时,slow指向中间,fast指向的是最后一个节点

当有偶数个节点时

slow
fast
1234NULL

此时,slow指向中间,fast指向NULL

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

注意:循环结束的条件是fast指向空者fast->next指向空

           则循环继续的条件是fastfast->next不指向空

合并两个有序链表

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

示例 1:


输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:

输入:l1 = [], l2 = []
输出:[]
示例 3:

输入:l1 = [], l2 = [0]
输出:[0]
 

提示:

两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/merge-two-sorted-lists

思路:找两个链表中值最小的进行尾插

  1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
  2. {
  3. if (list1 == NULL)
  4. {
  5. return list2;
  6. }
  7. else if (list2 ==NULL)
  8. {
  9. return list1;
  10. }
  11. else
  12. {
  13. struct ListNode* head = NULL;
  14. struct ListNode* tail = NULL;
  15. while (list1 != NULL && list2 != NULL)
  16. {
  17. if (list1->val >= list2->val)
  18. {
  19. if (tail == NULL)
  20. {
  21. head = tail = list2;
  22. }
  23. else
  24. {
  25. tail->next = list2;
  26. tail = tail->next;
  27. }
  28. list2 = list2->next;
  29. }
  30. else
  31. {
  32. if (tail == NULL)
  33. {
  34. head = tail = list1;
  35. }
  36. else
  37. {
  38. tail->next = list1;
  39. tail = tail->next;
  40. }
  41. list1 = list1->next;
  42. }
  43. }
  44. if (list1 != NULL)
  45. {
  46. tail->next = list1;
  47. }
  48. else
  49. {
  50. tail->next = list2;
  51. }
  52. return head;
  53. }
  54. }

有几个需要注意的点:
1.边界条件:list1或list2为空时,返回另一个链表

2.为什么要定义head和tail:

head:题目要求head作为头节点返回

tail:进行尾插时,如果没有tail,则需要多次遍历,徒增时间。不如定义一个tail指向最后一个节点

3.在尾插时,一定要注意tail是否为空,如果为空,则tail->next会报错,所以需要提前判断。

4.循环结束后,可能会出现的结果是,有一个链表没有值了,另一个链表还有节点,此时则需要直接将剩下的链表全部尾插即可。

由于判断tail是否为空,所以程序会出现冗余,下面提供两种方式优化代码

第一:在循环前就将head和tail先进行一次尾插

  1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
  2. {
  3. if (list1 == NULL)
  4. {
  5. return list2;
  6. }
  7. else if (list2 ==NULL)
  8. {
  9. return list1;
  10. }
  11. else
  12. {
  13. struct ListNode* head = NULL;
  14. struct ListNode* tail = NULL;
  15. if (list1->val <= list2->val)
  16. {
  17. head = tail = list1;
  18. list1 = list1->next;
  19. }
  20. else
  21. {
  22. head = tail = list2;
  23. list2 = list2->next;
  24. }
  25. while (list1 != NULL && list2 != NULL)
  26. {
  27. if (list1->val >= list2->val)
  28. {
  29. tail->next = list2;
  30. list2 = list2->next;
  31. }
  32. else
  33. {
  34. tail->next = list1;
  35. list1 = list1->next;
  36. }
  37. tail = tail->next;
  38. }
  39. if (list1)
  40. {
  41. tail->next = list1;
  42. }
  43. else
  44. {
  45. tail->next = list2;
  46. }
  47. return head;
  48. }
  49. }

第二:将单链表改成带头结点的链表,返回时只需返回head->next即可

malloc一个带有哨兵位的头节点,防止tail为空时,出现编译错误

最后注意,应当返回head->next

  1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
  2. {
  3. if (list1 == NULL)
  4. {
  5. return list2;
  6. }
  7. else if (list2 ==NULL)
  8. {
  9. return list1;
  10. }
  11. else
  12. {
  13. struct ListNode* head = NULL;
  14. struct ListNode* tail = NULL;
  15. //插入一个带哨兵位的头节点:如果防止tail为空时,tail->next出现编译错误
  16. head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
  17. while (list1 != NULL && list2 != NULL)
  18. {
  19. if (list1->val >= list2->val)
  20. {
  21. tail->next = list2;
  22. list2 = list2->next;
  23. }
  24. else
  25. {
  26. tail->next = list1;
  27. list1 = list1->next;
  28. }
  29. tail = tail->next;
  30. }
  31. if (list1)
  32. {
  33. tail->next = list1;
  34. }
  35. else
  36. {
  37. tail->next = list2;
  38. }
  39. //头节点不保存有效数据,所以应该返回头节点后,下一个节点
  40. return head->next;
  41. }

环形链表I

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

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

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

示例 1:

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

示例 2:

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

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
 

提示:

链表中节点的数目范围是 [0, 104]
-105 <= Node.val <= 105
pos 为 -1 或者链表中的一个 有效索引 。
 

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/linked-list-cycle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

快慢指针法

定义一个快指针fast和一个慢指针slow,

1.如果fast或者fast->next为空时,则没有环

2.如果fast和slow相遇时(相等),则有环

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

思考:

slow = slow->next时,fast = fast->next->next就一定能相遇吗?

如果是fast = fast->next->next->next呢?

提示:

和快慢指针的间距N和环的长度C有关

环形链表II

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

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

不允许修改 链表。

示例 1:

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

示例 2:

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

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
 

提示:

链表中节点的数目范围在范围 [0, 104] 内
-105 <= Node.val <= 105
pos 的值为 -1 或者链表中的一个有效索引
 

进阶:你是否可以使用 O(1) 空间解决此题?

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/linked-list-cycle-ii

思路:追击相遇问题

如同,若fast速度为slow的两倍

如果有环形链表,则此时fast先进入环

 当slow进入环内时,fast如图

当slow遍历的节点 < C 时,fast必会追上slow,定义相遇的节点为meet。

则slow遍历的节点为 Y + L,fast遍历的节点为 Y + N * C + L

则可列写方程

L + Y + N * C = 2 * (Y + L)

则 Y + L = N * C

∴ L = C - Y + (N - 1)* C

这里我们得到了一个很重要的结论:

从头节点开始遍历与从meet节点开始遍历的两个指针,肯定会在进环起点相遇

  1. struct ListNode *detectCycle(struct ListNode *head)
  2. {
  3. struct ListNode *fast = head, *slow = head;
  4. struct ListNode *meet = NULL;
  5. while (fast && fast->next)
  6. {
  7. fast = fast->next->next;
  8. slow = slow->next;
  9. if (fast == slow)
  10. {
  11. meet = fast;
  12. while (meet != head)
  13. {
  14. head = head->next;
  15. meet = meet->next;
  16. }
  17. return head;
  18. }
  19. }
  20. return NULL;
  21. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/779297
推荐阅读
相关标签
  

闽ICP备14008679号