• 《C语言数据结构》———链表进阶之双向链表


    在实际生活中,我们用到的最多的两种链表结构就是单链表和双向带头链表,上一篇已经介绍了单链表的实现以及一些应用,接下来我为大家详细介绍一下双向链表,以及一些链表oj题。

    文章目录

    • 一、双向链表的概念
    • 二、双向链表的实现
    • 三、链表与顺序表的差别
    • 四、链表oj
    • 总结


    提示:以下是本篇文章正文内容,下面案例可供参考

    一、双向链表的概念

    1、概念:概念:双向链表是每个结点除后继指针外还有⼀个前驱指针。双向链表也有带头结点结构和不带头结点结构两种,带头结点的双向链表更为常用;另外,双向链表也可以有循环和非循环两种结构,循环结构的双向链表更为常用。

    二、双向链表的实现

    头文件List.h

    1. #pragma once
    2. #include<stdio.h>
    3. #include<assert.h>
    4. #include<stdlib.h>
    5. typedef int LTDateType;
    6. typedef struct ListNode
    7. {
    8. LTDateType date;
    9. struct ListNode* next;
    10. struct ListNode* prev;
    11. }LTNode;
    12. //void ListInit(LTNode** pphead);
    13. void ListPrint(LTNode* phead);
    14. void ListPopBack(LTNode* phead);
    15. LTNode* ListInit();//初始化
    16. LTNode* BuyLTNode(LTDateType x);
    17. void ListPushBack(LTNode* phead, LTDateType x);
    18. void ListPushFront(LTNode* phead, LTDateType x);
    19. void ListPopFront(LTNode* phead);
    20. LTNode* ListFind(LTNode* phead, LTDateType x);
    21. //在pos前插入
    22. void ListInsert(LTNode* pos, LTDateType x);
    23. //删除pos位置的节点
    24. void ListErease(LTNode* pos);
    25. void ListDestory(LTNode* phead);

    源文件List.C

    1. #include"List.h"
    2. LTNode* BuyLTNode(LTDateType x)
    3. {
    4. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
    5. if (newnode == NULL)
    6. {
    7. printf("malloc fail\n");
    8. exit(-1);
    9. }
    10. newnode->date = x;
    11. newnode->next = NULL;
    12. newnode->prev = NULL;
    13. return newnode;
    14. }
    15. //void ListInit(LTNode** pphead)
    16. //{
    17. // assert(pphead);
    18. // *pphead = BuyLTNode(0);
    19. // (*pphead)->next = *pphead;
    20. // (*pphead)->prev = *pphead;
    21. //}
    22. LTNode* ListInit()
    23. {
    24. LTNode* phead = BuyLTNode(0);
    25. phead->next = phead;
    26. phead->prev = phead;
    27. return phead;
    28. }
    29. void ListPrint(LTNode* phead)
    30. {
    31. assert(phead);
    32. LTNode* cur = phead->next;
    33. while (cur != phead)
    34. {
    35. printf("%d ", cur->date);
    36. cur = cur->next;
    37. }
    38. printf("\n");
    39. }
    40. void ListPushBack(LTNode* phead, LTDateType x)
    41. {
    42. assert(phead);
    43. LTNode* tail = phead->prev;
    44. LTNode* newnode = BuyLTNode(x);
    45. tail->next = newnode;
    46. newnode->prev = tail;
    47. newnode->next = phead;
    48. phead->prev = newnode;
    49. }
    50. void ListPushFront(LTNode* phead, LTDateType x)
    51. {
    52. assert(phead);
    53. ListInsert(phead->next, x);
    54. }
    55. void ListPopBack(LTNode* phead)//尾删
    56. {
    57. assert(phead);
    58. assert(phead->next != phead);//说明传进来的不只有phead,不然phead被free掉了。
    59. //LTNode* tail = phead->prev;
    60. //LTNode* tailPrev = tail->prev;
    61. //free(tail);
    62. //tail = NULL;
    63. //tailPrev->next = phead;
    64. //phead->prev = tailPrev;
    65. ListErease(phead->prev);
    66. }
    67. void ListPopFront(LTNode* phead)//头删
    68. {
    69. assert(phead);
    70. assert(phead->next != phead);
    71. ListErease(phead->next);
    72. }
    73. LTNode* ListFind(LTNode* phead, LTDateType x)
    74. {
    75. assert(phead);
    76. LTNode* cur = phead->next;
    77. while (cur != phead)
    78. {
    79. if (cur->date == x)
    80. {
    81. return cur;
    82. }
    83. cur = cur->next;
    84. }
    85. return NULL;
    86. }
    87. //void ListInsert(LTNode* pos, LTDateType x)
    88. //{
    89. // assert(pos);
    90. // LTNode* newnode = BuyLTNode(x);
    91. // pos->prev->next = newnode;
    92. // newnode->prev = pos->prev;
    93. //
    94. // pos->prev = newnode;
    95. // newnode->next = pos;
    96. //
    97. //}
    98. //更好的写法
    99. void ListInsert(LTNode* pos, LTDateType x)
    100. {
    101. assert(pos);
    102. //无关写的顺序
    103. LTNode* newnode = BuyLTNode(x);
    104. LTNode* posPrev = pos->prev;
    105. newnode->next = pos;
    106. pos->prev = newnode;
    107. posPrev->next = newnode;
    108. newnode->prev = posPrev;
    109. }
    110. // 驼峰法
    111. //函数和类型所以单词首字母大写
    112. //变量:第一个单词小写后续单词首字母大写
    113. void ListErease(LTNode* pos)
    114. {
    115. assert(pos);
    116. LTNode* prev = pos->prev;
    117. LTNode* next = pos->next;
    118. free(pos);
    119. //此时要把pos置成空,不然pos就是野指针了
    120. pos = NULL;
    121. prev->next = next;//LT的新的prev指向pos->next;
    122. next->prev = prev;//LT的新的next的prev指向prev;
    123. }
    124. void ListDestory(LTNode* phead)
    125. {
    126. assert(phead);
    127. LTNode* cur = phead->next;//从头结点的下一个位置开始。
    128. while (cur != phead)
    129. {
    130. LTNode* next = cur->next;//先记录,再free
    131. //ListErease(cur);//副用Erease函数实现destory
    132. free(cur);//
    133. cur = next;
    134. }
    135. free(phead);
    136. //phead = NULL;形参的改变不影响实参
    137. }

    三、链表与顺序表的差别

    不同点顺序表链表
    存储空间上物理上一定连续逻辑上连续,但物理上不一定连 续
    随机访问支持O(1)不支持:O(N)
    任意位置插入或者删除元 素可能需要搬移元素,效率低O(N)只需修改指针指向
    插入动态顺序表,空间不够时需要扩容没有容量的概念
    应用场景元素高效存储+频繁访问任意位置插入和删除频繁
    缓存利用率

     四、链表oj

    1、链表中倒数第k个结点_牛客题霸_牛客网(链接)

    思路:快慢指针法,fast先走k步, slow和fast同时走, fast走到尾时,slow就是倒数第k个

    代码示例:

    1. struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    2. struct ListNode* slow, *fast;
    3. slow = fast = pListHead;
    4. while(k --)//走k步
    5. {
    6. //判断K是否大于链表的长度。
    7. if(fast == NULL) return NULL;
    8. fast = fast->next;
    9. }
    10. while(fast)//当fast 指针走到尾时
    11. {
    12. slow = slow->next;
    13. fast = fast->next;
    14. }
    15. return slow;
    16. }
    17. 第二种写法:
    18. struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
    19. {
    20. struct ListNode* p1 = NULL, *p2 = NULL;
    21. p1 = pListHead;
    22. p2 = pListHead;
    23. int a = k;
    24. int c = 0;//记录节点个数
    25. //p1指针先跑,并且记录节点数,当p1指针跑了k-1个节点后,p2指针开始跑,
    26. //当p1指针跑到最后时,p2所指指针就是倒数第k个节点
    27. while(p1)//当p1 不为空时
    28. {
    29. p1 = p1->next;
    30. c ++;//节点个数增加
    31. if(k < 1) p2 = p2->next;
    32. k --;
    33. }
    34. if(c < a) return NULL;
    35. return p2;
    36. }

      2、21. 合并两个有序链表(链接)
    思路:归并的思想,将小的值尾插到新链表。时间复杂度为n^2;如果再定义一个尾指针, 每次记录尾。

    1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
    2. {
    3. if(list1 == NULL)//list1和list2分别是两个链表的头指针
    4. return list2;
    5. if(list2 == NULL)
    6. return list1;
    7. struct ListNode* head = NULL, *tail = NULL;//head是新链表的头指针,tail是新链表的尾指针
    8. while(list1 && list2)
    9. {
    10. if(list1->val < list2->val)
    11. {
    12. if(tail == NULL)//这一步不太理解
    13. {
    14. head = tail = list1;
    15. }
    16. else
    17. {
    18. tail->next = list1;//先传指针指向
    19. tail = list1;//再把list 的地址传给tail,相当于tail往前挪了一步。
    20. }
    21. list1 = list1->next;
    22. }
    23. else
    24. {
    25. if(tail == NULL)
    26. {
    27. head = tail = list2;
    28. }
    29. else
    30. {
    31. tail->next = list2;
    32. tail = list2;//传地址
    33. }
    34. list2 = list2->next;
    35. }
    36. }
    37. if(list1)
    38. {
    39. tail->next = list1;//如果链表1不为空,而此时链表二为空,则直接把链表二的值连接过去就好了。
    40. }
    41. if(list2)
    42. {
    43. tail->next = list2;
    44. }
    45. return head;
    46. }
    47. 方法二:设置一个哨兵位头结点
    48. head存随机值, head->next存第一个链表的值。起到简化代码的作用。
    49. 一般的链表没有设置哨兵位头结点。
    50. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
    51. {
    52. struct ListNode* head = NULL, *tail = NULL;
    53. head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
    54. head->next = NULL;
    55. while(list1 && list2)
    56. {
    57. if(list1->val < list2->val)
    58. {
    59. tail->next = list1;//先传指针指向
    60. tail = list1;//再把list 的地址传给tail,相当于tail往前挪了一步。
    61. list1 = list1->next;
    62. }
    63. else
    64. {
    65. tail->next = list2;
    66. tail = list2;//传地址
    67. list2 = list2->next;
    68. }
    69. }
    70. if(list1)
    71. {
    72. tail->next = list1;//如果链表1不为空,而此时链表二为空,则直接把链表二的值连接过去就好了。
    73. }
    74. if(list2)
    75. {
    76. tail->next = list2;
    77. }
    78. //解决内存泄漏问题;
    79. struct ListNode* list = head->next;
    80. free(head);
    81. return list;
    82. }

    3、链表分割_牛客题霸_牛客网(链接)

    思路:新设置两个链表,小于x的插到第一个链表,大于x的插到第二个链表。

    定义四个指针:LessHead, LessTail,GreatHead, GreatTail.

    原链表的值分别尾插到这两个链表, 然后分别更新LessTail,和GreatTail;

    最后LessTail的指针再指向GreatHead。完成两个链表的连接。

    想极端场景:

    1、所有值比x小

    2、所有值比x大

    3、比x大和小都有,最后一个值是比x小

    4、比x大和小都有,最后一个值是比x大

     


    1. 构成环链表,造成死循环。
    2. //设置哨兵位
    3. class Partition {
    4. public:
    5. ListNode* partition(ListNode* pHead, int x) {
    6. struct ListNode* lessHead, *lessTail, *greatHead, *greatTail;
    7. lessHead = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));
    8. greatHead = greatTail = (struct ListNode*)malloc(sizeof(struct ListNode));
    9. lessTail->next = greatTail->next = NULL;
    10. struct ListNode* cur = pHead;
    11. while (cur) {
    12. if (cur->val < x) {
    13. lessTail->next = cur;
    14. lessTail = lessTail->next;
    15. } else {
    16. greatTail->next = cur;
    17. greatTail = greatTail->next;
    18. }
    19. cur = cur->next;
    20. }
    21. //完成链接工作
    22. lessTail->next = greatHead->next;
    23. greatTail->next = NULL;//切断greetTail的最后与greetHead的链接
    24. struct ListNode* list = lessHead->next;
    25. free(lessHead);
    26. free(greatHead);
    27. return list;
    28. }
    29. };

    4、链表的回文结构_牛客题霸_牛客网 (链接)

     思路:找出链表的中间节点, 然后逆置,判断前后链表是否一致,若一致,则说明该链表是回文结构。

    为什么奇数个时也能过,

    例如:1 2 3 2 1

    逆置完变为了 1 2  1 2  3 ;

    当A走到2的位置时2->next = 3, rHead走到的 2->next = 3, 相等。

     

    1. class PalindromeList {
    2. public:
    3. struct ListNode* middleNode(struct ListNode* head)
    4. {
    5. struct ListNode* slow, * fast;
    6. slow = fast = head;
    7. while(fast && fast->next) //想的是结束的条件,写的是继续的条件
    8. {
    9. slow = slow->next;
    10. fast = fast->next->next;
    11. }
    12. return slow;
    13. }
    14. struct ListNode* reverseList(struct ListNode* head)
    15. {
    16. struct ListNode* NewHead = NULL;
    17. struct ListNode* cur = head;
    18. while(cur)
    19. {
    20. struct ListNode* next = cur->next;
    21. //头插
    22. cur->next = NewHead;//更多代表链表的指向方向。
    23. NewHead = cur;//接着把地址传过来(更新)
    24. cur = next;//(更新)
    25. }
    26. return NewHead;
    27. }
    28. bool chkPalindrome(ListNode* A) {
    29. struct ListNode* mid = middleNode(A);
    30. struct ListNode*rHead = reverseList(mid);//应该逆置mid,中间结点。
    31. while(A && rHead)
    32. {
    33. if(A->val == rHead->val)
    34. {
    35. A = A->next;
    36. rHead = rHead->next;
    37. }
    38. else
    39. {
    40. return false;
    41. }
    42. }
    43. return true;
    44. }
    45. };

     5、力扣相交链表(链接)

    思路1:A链表每个节点B跟链表依次比较,如果有相等,就是相交,第一个相等就是交点。

    时间复杂度:o(N*M);

    思路二:如果两个链表相交,则这两个链表的最后一个元素的地址相同。

    包含思路二三:

    代码示例:

    1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
    2. {
    3. struct ListNode* tailA, *tailB;//因为之后还要用到headA,和headB,所以要用tail来进行迭代。
    4. tailA = headA, tailB = headB;
    5. int lenA = 1, lenB = 1;
    6. while(tailA)//求出A的尾
    7. {
    8. tailA = tailA->next;
    9. ++lenA;//求出A的长度
    10. }
    11. while(tailB)//求出链表B的尾
    12. {
    13. tailB = tailB->next;
    14. ++lenB;//求出B的长度
    15. }
    16. if(tailA != tailB)//如果两个链表的尾不相等,则返回空
    17. {
    18. return NULL;
    19. }
    20. //相交,求交点,长的先走差距步,再同时找交点。
    21. struct ListNode* shortList = headA, *longList = headB;//默认A短B长
    22. if(lenA > lenB)
    23. {
    24. shortList = headB;
    25. longList = headA;
    26. }
    27. int gap = abs(lenA - lenB);//求出差距
    28. while(gap--)//减gap次,若是--gap,就是减了gap - 1次
    29. {
    30. longList = longList->next;
    31. }
    32. while(shortList && longList)
    33. {
    34. if(shortList == longList)
    35. return shortList;//此时shortList == longList;
    36. longList = longList->next;
    37. shortList = shortList->next;
    38. }
    39. //最最关键的一步:就是要在外面返回一个值,因为编译器不会判断代码在哪返回,只会检查你的代码的语法问题。
    40. return NULL;
    41. }

    总结

     

    本文分别从双向链表的概念、实现,还比较了顺序表和链表的区别,以及5道链表oj题四个方面介绍了链表进阶的知识,希望大家读后能够有所收获。

  • 相关阅读:
    【 OpenGauss源码学习 —— 列存储(CU)(二)】
    在数据框中如何把变量定义为整数型数据
    分布式事务最经典的八种解决方案
    金九银十,23届秋招信息超全汇总表!附各大名企优质岗位内推码!持续更新中···
    Mysql 高阶语句
    Linux内核--链表结构
    mysql的分组group by
    红黑树2——怎么画红黑树
    【UN-JS-工具类】懒加载的实现 -- 两种方式 --- 一种5行JS实现懒加载
    计算机毕业设计springboot基于大数据的疫情追踪系统的设计和实现rva1s源码+系统+程序+lw文档+部署
  • 原文地址:https://blog.csdn.net/qq_62662919/article/details/124809769