• 【数据结构初阶】单链表补充内容+又双叒叕刷链表题


    目录

    1.顺序表&链表的优点和缺点

    2.单链表和双向循环链表

    3.一点杂七杂八的东西

    3-1顺序表和链表打印的断言

    3-2.栈上和堆上定义一个新节点 

    3-3.二级指针 

    3-4.哨兵头结点的作用

    3-5 为什么单链表的基本操作中无tail记录尾

    4.替换法删除pos结点

    4-1.变式

    5 .链表调试

    6.为什么学校老师讲的时候不先讲带头结点的

    7.刷刷刷题

    7-1.回文链表

    7-2.相交链表

    7-3. 链表分割


    1.顺序表&双向循环链表的优点和缺点

    顺序表:

    一.优点:

    1. 尾插尾删效率很高
    2. 支持用下标随机访问

    二.缺点:

    1. 头部和中部插入和删除效率低O(n)
    2. 扩容-----性能消耗+空间消耗

    双向循环链表:

    一.优点:

    1. 任意位置插入删除效率很高O(1)
    2. 按需申请释放

    二.缺点:

    1. 不支持随机访问

    综合而言,两个各有优缺,相辅相成,具体用谁看场景,但是总体而言顺序表使用的频率更高一点

    扩展一点:

    顺序表的cpu高速缓存命中率高(顺序表空间连续)

     

    2.单链表和双向循环链表

    1.单链表:其他结构的子结构(比如哈希桶),面试经常问到和OJ题目,且只适合头部的插入删除。

    2.双向循环链表:结构复杂,但是实现简单,最为实用,常被用于实际存数据,适合任意位置的插入删除。

    3.一点杂七杂八的东西

    3-1顺序表和链表打印的断言

    1. 1.顺序表和链表打印断言
    2. 顺序表定义:SeqList Sq;
    3. 在传给打印函数的时候:SeqListPrint(&Sq)
    4. 在函数内部的时候&Sq一定不为空,void SeqListPrint(SeqList* ps)所以要断言一下assert(ps);
    5. 单链表定义变量: SLTNode* phead;
    6. 在传给打印函数的时候:SLTNodePrint(&phead);
    7. 在函数内部打印的时候phead为空的时候,&phead也可能为空,所以不能断言

    但是注意:虽然phead为空,但是pphead不为空 ,所以在链表头插函数内部还是要对pphead断言。

    原因:phead没有指向任何内容,但是phead本身是一个指针变量,是变量就有地址。

     

    3-2.栈上和堆上定义一个新节点 

    1. 2.栈(错)和堆上(对)定义一个新节点
    2. void SLTNodePushFront(SLTNode** pphead,STDateType x)
    3. {
    4. //错误示范:栈上开辟的,出函数销毁
    5. SLTNode newnode;
    6. //正确做法:malloc结点,堆上开辟,空间需要手动释放
    7. SLTNode* newhead=(SLTNode*)malloc(sizeof(SLTNode));
    8. }

    3-3.二级指针 

    我们在主函数定义了SListNode*  plist;

    在改变plist的指向的时候就要传二级指针,但是如果在函数内改结点,直接用一级指针就能改,就像我们在函数内部定义的prev,cur等。这也就是哨兵位头结点的原理,在因为有哨兵位的头结点,就不用改plist,所以可以在主函数内传一级指针。

    1. 3.二级指针
    2. 以尾插函数为例,如果主函数里的plist为空,那么要改plist就要传二级指针
    3. 但是为什么在plist不为空的时候,要尾插一个
    4. void SLTNodePushBack(SLTNode* pphead, SLTDateType x)//1
    5. {
    6. assert(pphead);//2
    7. SLTNode* newhead = (SLTNode*)malloc(sizeof(SLTNode));
    8. newnode->next = NULL;
    9. newnode->date = x;
    10. if (*pphead == NULL)
    11. {
    12. *pphead = newnode;
    13. }
    14. else
    15. {
    16. SLTNode* tail = *pphead;
    17. while (tail)
    18. {
    19. tail = tail->next;
    20. }
    21. tail->next = newnode;//3
    22. }
    23. }

    无头单向不循环链表传二级指针发挥作用的情况:

    • 没有结点尾插
    • 只有一个结点尾删
    • 头插
    • 头删
    • 销毁

    3-4.哨兵头结点的作用

    头结点的作用:

    1. 方便对plist为空等特殊情况时的统一操作
    2. 避免传二级指针

    备注:有人说哨兵位头结点的数据域是用来存储单链表的长度;

    专业打假:其实这种说法是错误的,因为结点的数据域为char类型的且链表长度大于127的时候就会溢出,所以这种说法是错误的。

    3-5 为什么单链表的基本操作中无tail记录尾

    看过我写题的应该知道,在建立新链表的时候,通常是采用尾插操作,尾插相对于头插能够保证新链表的结点在原链表的相对顺序不变,但是尾插的缺点是每次尾插都要找尾,所以我们定义一个尾指针,实时更新尾指针。

    那为什么单链表的基本操作中无tail记录尾?那是因为在基本操作中不止有尾插,还有尾删,定义一个tail效果适用性不是很强。

    4.替换法删除pos结点

     

    基于单链表删除pos位置还要找尾的麻烦,删除pos位置的结点有一个替换法删除,数据域值交换的方法,时间复杂度是O(1):

    如果要删除pos位置的结点,则可以狸猫换太子把pos后面一个结点的值给pos位置的数据域,然后pos->next=pos->next->next,也就是把pos后面那个结点作替死鬼。

    缺点:pos位置不能是尾结点

    pos位置是尾结点且链表是循环链表就可以,但是如果是循环链表的话就没必要使用替换法删除pos位置的结点.

    4-1.变式

    如果要在pos位置之前插入一个结点,时间复杂度为O(1),也可以采用这种方法:

    BuySListNode(pos->val);

    pos->val=x;

    5 .链表调试

    当OJ写不出来,想在VS上调试代码,找Bug,但是每次还要重建链表?有什么解决办法?

    备注:程序员一半的时间都在改Bug,你连调试都不会,就等着扣绩效吧😁😁

    解决办法:在桌面上备份一份单链表的代码,方便OJ调试。(代码如下)

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. struct ListNode
    3. {
    4. int val;
    5. struct ListNode* next;
    6. };
    7. #include
    8. #include
    9. int main()
    10. {
    11. struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
    12. struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
    13. struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
    14. struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
    15. struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
    16. struct ListNode* n6 = (struct ListNode*)malloc(sizeof(struct ListNode));
    17. struct ListNode* n7 = (struct ListNode*)malloc(sizeof(struct ListNode));
    18. n1->val = 1;
    19. n2->val = 2;
    20. n3->val = 3;
    21. n4->val = 4;
    22. n5->val = 5;
    23. n6->val = 6;
    24. n7->val = 7;
    25. n1->next = n2;
    26. n2->next = n3;
    27. n3->next = n4;
    28. n4->next = n5;
    29. n5->next = n6;
    30. n6->next = n7;
    31. n7->next = NULL;
    32. return 0;
    33. }

    6.为什么学校老师讲的时候不先讲带头结点的

    1. 实际应用中很少带头
    2. OJ题的head大大部分都是不带头的

    7.刷刷刷题

    上次的链表题刷的过瘾吗?链表习题集1

    我又双叒叕来了,记住题目是环环相扣的,记得先把习题集1做了哦,有些解法这里用到将不会再细讲...,敬请谅解 

    7-1.回文链表

    OR36 链表的回文结构

     思路1:

    1. 第一步:利用快慢指针找到链表的中间结点slow
    2. 第二步:将中间结点开始以后的结点部分逆置(反转),得到相交链表的新头指针prev
    3. 第三步:遍历判断两个链表是否值相等

    反转过程是最重要的部分,这里给出一点图理解一下

    1. class PalindromeList {
    2. public:
    3. bool chkPalindrome(ListNode* phead) {
    4. // 第一步:快慢指针求中间结点slow
    5. struct ListNode* fast,*slow;
    6. fast=slow=phead;
    7. while(fast&&fast->next)
    8. {
    9. fast=fast->next->next;
    10. slow=slow->next;
    11. }
    12. //第二步:部分反转,得新头结点prev
    13. struct ListNode* prev=NULL;
    14. struct ListNode* cur=slow;
    15. while(cur)
    16. {
    17. struct ListNode* next=cur->next;
    18. cur->next=prev;
    19. prev=cur;
    20. cur=next;
    21. }
    22. //第三步:判断新旧链表是否相等
    23. struct ListNode* newhead=prev;
    24. struct ListNode* oldhead=phead;
    25. while(newhead)
    26. {
    27. if(newhead->val!=oldhead->val)
    28. {
    29. return false;
    30. }
    31. newhead=newhead->next;
    32. oldhead=oldhead->next;
    33. }
    34. return true;
    35. }
    36. };

    这里其实形参名是可以改成自己喜欢的名字,而不是题目这么挫的A 

    思路2:牢牢抓住回文链表的定义,从前往后和从后往前读都是一样的

    1. 第一步:拷贝原链表,得到头指针copyhead
    2. 第二步:将原链表整体反转,得到r头指针eversehead
    3. 第三步:遍历判断两个链表是否值相等

    (备注:这里一定一定要记得先拷贝一份原链表,否则反转后得到的链表将不再是原先那个链表!)

    1. class PalindromeList {
    2. public:
    3. //动态开辟一个新节点,新节点的值为cur->val
    4. struct ListNode* BuySListNode(int val)
    5. {
    6. struct ListNode* newnode=(struct ListNode*)malloc(sizeof(struct ListNode));
    7. newnode->val=val;
    8. newnode->next=NULL;
    9. return newnode;
    10. }
    11. bool chkPalindrome(ListNode* phead) {
    12. //第一步:拷贝原链表,得到copyhead
    13. struct ListNode* Guard,*Tail;
    14. Guard=Tail=(struct ListNode*)malloc(sizeof(struct ListNode));
    15. struct ListNode* cur=phead;
    16. while(cur)
    17. {
    18. struct ListNode* next=cur->next;
    19. struct ListNode* newnode=BuySListNode(cur->val);
    20. Tail->next=newnode;
    21. Tail=Tail->next;
    22. cur=next;
    23. }
    24. struct ListNode* copyhead=Guard->next;
    25. free(Guard);
    26. Guard=NULL;
    27. //第二步:将原链表整体反转,得到reversehead
    28. struct ListNode* prev=NULL;
    29. struct ListNode* cur2=phead;
    30. while(cur2)
    31. {
    32. struct ListNode* next=cur2->next;
    33. cur2->next=prev;
    34. prev=cur2;
    35. cur2=next;
    36. }
    37. struct ListNode* reversehead=prev;
    38. //第三步:遍历判断两个链表是否值相等
    39. while(copyhead)
    40. {
    41. if(copyhead->val!=reversehead->val)
    42. {
    43. return false;
    44. }
    45. copyhead=copyhead->next;
    46. reversehead=reversehead->next;
    47. }
    48. return true;
    49. }
    50. };

    7-2.相交链表

    力扣之相交链表

    题目的相交链表可能有的同学会误以为是X字形,但是却只有Y字形,原因是每一个结点只有一个指针域,没有两个指针域。 

    思路:

    1. 第一步:分别遍历两个链表,分别求出两个链表的长度
    2. 第二步:让长度长的那个链表先走两链表长度的差距步
    3. 第三步:短链表从头开始走,长链表从差距步开始同步走
    4. 第四步:相遇点即是相交链表的交点结点,返回
    1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    2. //第一步:分别遍历两个链表,分别求出两个链表的长度
    3. int lenA=1,lenB=1;
    4. struct ListNode* TailA=headA;
    5. struct ListNode* TailB=headB;
    6. while(TailA->next)
    7. {
    8. ++lenA;
    9. TailA=TailA->next;
    10. }
    11. while(TailB->next)
    12. {
    13. ++lenB;
    14. TailB=TailB->next;
    15. }
    16. if(TailA!=TailB)
    17. {
    18. return false;
    19. }
    20. //第二步:让长度长的那个链表先走两链表长度的差距步
    21. struct ListNode* longhead=headA;
    22. struct ListNode* shorthead=headB;
    23. if(lenA
    24. {
    25. longhead=headB;
    26. shorthead=headA;
    27. }
    28. int div=abs(lenA-lenB);
    29. struct ListNode* cur1=longhead;
    30. struct ListNode* cur2=shorthead;
    31. while(div--)
    32. {
    33. cur1=cur1->next;
    34. }
    35. //第三步:短链表从头开始走,长链表从差距步开始同步走
    36. while(cur1!=cur2)
    37. {
    38. cur1=cur1->next;
    39. cur2=cur2->next;
    40. }
    41. //第四步:相遇点即是相交链表的交点结点,返回
    42. return cur1;
    43. }

    7-3. 链表分割

    链表分割

    思路:将链表结点按数据值=x尾插到相应链表 ,然后再将两分割的链表连接起来。

    1. 第一步:定义大小Guard和Tail指针
    2. 第二步:   遍历尾插到相应新链表
    3. 第三步: 连接两个分割的链表

    备注:记得将bigTail->next置空!!!否则会运行超时

    1. class Partition {
    2. public:
    3. ListNode* partition(ListNode* pHead, int val) {
    4. // write code here
    5. //第一步:定义大小Guard和Tail指针
    6. struct ListNode* lessGuard,*lessTail;
    7. lessGuard=lessTail=(struct ListNode*)malloc(sizeof(struct ListNode));
    8. lessGuard->next=NULL;
    9. struct ListNode* bigGuard,*bigTail;
    10. bigGuard=bigTail=(struct ListNode*)malloc(sizeof(struct ListNode));
    11. bigGuard->next=NULL;
    12. //第二步:   遍历尾插到相应新链表
    13. struct ListNode* cur=pHead;
    14. while(cur)
    15. {
    16. struct ListNode* next=cur->next;
    17. if(cur->val
    18. {
    19. lessTail->next=cur;
    20. lessTail=lessTail->next;
    21. }
    22. else
    23. {
    24. bigTail->next=cur;
    25. bigTail=bigTail->next;
    26. }
    27. cur=next;
    28. }
    29. //第三步: 连接两个分割的链表
    30. lessTail->next=bigGuard->next;
    31. bigTail->next=NULL;
    32. struct ListNode* newhead=lessGuard->next;
    33. free(lessGuard);
    34. free(bigGuard);
    35. lessGuard=NULL;
    36. bigGuard=NULL;
    37. return newhead;
    38. }
    39. };

  • 相关阅读:
    是什么引起数据库响应超时?
    请给出python程序运行结果
    从Github上整理下来的《Java面试神技》
    35、商户查询缓存(利用互斥锁解决缓存击穿)
    多线程JUC 第2季 synchronized锁升级过程
    Qt应用程序打包步骤(完美解决)
    ThreeDPoseTracker项目解析
    数字化时代,数据仓库是什么?有什么用?
    系统编程07-线程的互斥锁、读写锁、条件变量
    Appium入门自动化测试(4) —— Appium常用的手势操作API
  • 原文地址:https://blog.csdn.net/qq_64428099/article/details/126048709