深圳幻海软件技术有限公司 欢迎您!

数据结构——单链表(下)

2023-03-14

🌇个人主页:_麦麦_📚今日名言:年轻时候的我以为坚持是永不动摇,到这个年纪明白了坚持就是犹疑着,退缩着,心猿意马着,一步三停着,还在往前走。——《十二月历》目录一、引言 二、单链表剩余功能的实现    1.单链表的查找  &n

🌇个人主页:_麦麦_

📚今日名言:年轻时候的我以为坚持是永不动摇,到这个年纪明白了坚持就是犹疑着,退缩着,心猿意马着,一步三停着,还在往前走。——《十二月历》

目录

一、引言

 二、单链表剩余功能的实现

        1.单链表的查找

        2. 单链表指定位置(pos)的前插

        3. 单链表指定位置(pos)的删除

        4.单链表指定位置(pos)的后插

        5.单链表指定位置(pos)的后删

         6.单链表的销毁

三、 与链表相关的练习题

1.删除表中等于给定值val的所有结点(力扣)

2.链表的中间结点(力扣)

 3.链表的倒数第k个结点(牛客)

四、结语


一、引言

         许久未更,甚是想念。今日为大家带来的是单链表剩余的功能实现以及一些有关链表的代码练习,希望能够为读者带来一点点收获,便心满意足了。

 二、单链表剩余功能的实现

        1.单链表的查找

        在这期博文中我们会对单链表指定位置的插入和删除进行实现,熟悉单链表的小伙伴一定知道无论是插入还是删除,我们都需要寻找到指定数值所对应的位置指针,因此我们将寻找指定位置指针的过程封装成一个函数,避免了代码的重复化

        那么该如何根据指定值找到对应的指针呢?其实并不复杂,只需要对链表进行遍历并对每个结点的值进行判断,若是与指定值相符,便将该结点的地址返回。

        具体代码实现如下:

  1. //寻找函数的声明
  2. SListNode* SListFind(SListNode* phead, SLTDataType x);
  3. //寻找函数的具体实现
  4. SListNode* SListFind(SListNode* phead, SLTDataType x)
  5. {
  6. SListNode *cur = phead;
  7. while (cur)
  8. {
  9. if (cur->data == x)
  10. {
  11. return cur;
  12. }
  13. cur = cur->next;
  14. }
  15. return NULL;
  16. }

        2. 单链表指定位置(pos)的前插

        由于单链表的查找函数只能返回指定位置的地址,而通过这个地址我们并不能访问它的前一个结点,自然也就不能在其之前插入一个新的结点,因此只能通过遍历链表的方式找到指定位置的前一个结点,再进行新结点的插入

        不过要注意的一点是如果pos是单链表的首个结点,那么其实现逻辑与单链表的头插相同,所以只需调用头插函数即可。

  1. //前插函数声明
  2. void SLTInsert(SListNode** pphead, SListNode*pos, SLTDataType x);
  3. //前插函数具体实现
  4. void SLTInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
  5. {
  6. assert(pos);
  7. assert(pphead);
  8. SListNode* newnode = BuySLTNode(x);
  9. SListNode* cur = *pphead;
  10. if (pos ==*pphead)
  11. {
  12. SLTPushFront(pphead, x);
  13. }
  14. while (cur)
  15. {
  16. if (cur->next == pos)
  17. {
  18. cur->next = newnode;
  19. newnode->next = pos;
  20. }
  21. cur = cur->next;
  22. }
  23. }

        3. 单链表指定位置(pos)的删除

        单链表指定位置的删除与前插类似,需要通过遍历链表找到指定位置的之前的一个结点,并通过其将指定位置的节点删除。

        不过若是pos为链表的首个结点其实现就与单链表的头删相同,因此只需调用单链表的头删函数即可。

  1. //SLTErase函数的声明
  2. void SLTErase(SListNode** pphead, SListNode* pos);
  3. //SLTErase函数的实现
  4. void SLTErase(SListNode** pphead, SListNode* pos)
  5. {
  6. assert(pphead);
  7. assert(pos);
  8. SListNode* prev = *pphead;
  9. //头插
  10. if (pos == *pphead)
  11. {
  12. SLTPopFront(pphead);
  13. }
  14. else
  15. {
  16. //找到pos的前一个位置
  17. while (prev)
  18. {
  19. if (prev->next == pos)
  20. {
  21. SListNode* Erase = prev->next;
  22. prev->next = prev->next->next;
  23. free(Erase);
  24. Erase = NULL;
  25. }
  26. prev = prev->next;
  27. }
  28. }
  29. }

        4.单链表指定位置(pos)的后插

        相比于前插,后插在单链表中更为常见,也更为简单。只需要将pos所指向的下一个结点更改为新插入的结点,而新插入的结点则指向原pos的下一个结点。 

  1. //SListInsertAfter的声明
  2. void SListInsertAfter(SListNode* pos, SLTDataType x);
  3. //SListInsertAfter的实现
  4. void SListInsertAfter(SListNode* pos, SLTDataType x)
  5. {
  6. assert(pos);
  7. SListNode* newnode = BuySLTNode(x);
  8. newnode->next = pos->next;
  9. pos->next = newnode;
  10. }

        5.单链表指定位置(pos)的后删

         单链表指定位置的后删相较于指定位置的删除也更为简单,大体思路就是将指定位置的结点指向要删除的结点的下一个结点,并将要删除的结点释放

  1. //SListEraseAfter函数的声明
  2. void SListEraseAfter(SListNode* pos);
  3. //SListEraseAfter函数的实现
  4. void SListEraseAfter(SListNode* pos)
  5. {
  6. assert(pos);
  7. SListNode* del = pos->next;
  8. pos->next = pos->next->next;
  9. free(del);
  10. del = NULL;
  11. }

         6.单链表的销毁

        在销毁这个功能的实现上,采取的是一边遍历链表,一边对链表的结点进行释放。不过要注意的一点是如果传来的是NULL指针,就不需要进行销毁的操作了。

  1. //SListDestroy函数的声明
  2. void SListDestroy(SListNode** pphead);
  3. //SListDestroy函数的实现
  4. void SListDestroy(SListNode** pphead)
  5. {
  6. assert(pphead);
  7. SListNode* cur=*pphead;
  8. SListNode* next = *pphead;
  9. //判断是否为空链表
  10. if (cur == NULL)
  11. {
  12. return;
  13. }
  14. else
  15. {
  16. while (cur->next)
  17. {
  18. next = cur->next;
  19. free(cur);
  20. cur = next;
  21. }
  22. printf("链表已销毁\n");
  23. return;
  24. }
  25. }

         至此为止,但单链表基础功能的实现已告一段落,有兴趣的小伙伴可以继续深入,根据自己的需求写出另外的功能。接下来是几个与链表有关的编程题目讲解,以供大家学习和参考。

三、 与链表相关的练习题

1.删除表中等于给定值val的所有结点(力扣)

         本文采取一种较为传统的方法来对此题进行解答,即双指针法。通过“prev”和“cur”两个指针对链表进行遍历,若是cur的val值与所给值相符,即将cur的空间释放,并进行两个指针的移动。

        不过若是按照以上思路来编写代码,会发现当链表的首个结点的val值就是所给值时,会对prev指针进行访问,而此时的prev指针还是空指针,就会出现对空指针的访问,显然是会出现问题的。这时候采取的方案就和我们在单链表中实现的头删类似,具体的代码如下:

  1. //结构体定义
  2. /**
  3. * Definition for singly-linked list.
  4. * struct ListNode {
  5. * int val;
  6. * struct ListNode *next;
  7. * };
  8. */
  9. struct ListNode* removeElements(struct ListNode* head, int val)
  10. {
  11. struct ListNode* cur=head;
  12. struct ListNode* prev=NULL;
  13. while(cur)
  14. {
  15. if(cur->val!=val)
  16. {
  17. prev=cur;
  18. cur=cur->next;
  19. }
  20. else
  21. {
  22. //头删
  23. if(prev==NULL)
  24. {
  25. head=cur->next;
  26. free(cur);
  27. cur=head;
  28. }
  29. else
  30. {
  31. prev->next=cur->next;
  32. free(cur);
  33. cur=prev->next;
  34. }
  35. }
  36. }
  37. return head;
  38. }

2.链表的中间结点(力扣)

         这道题较为传统的解法是先对链表进行遍历来记录下链表的结点个数,再根据结点个数进行第二次链表遍历来寻找到中间结点。但倘若只能对链表进行一次遍历,小伙伴们有什么好的想法吗?

        对此,我们采用了一个非常巧妙的“快慢指针”法,即设立两个指针,一个每次走两个结点,而另一个每次走一个结点,当走的快的指针走到末尾,慢的指针此时就走到了链表的中间结点。

        不过由于链表的结点的个数可能为奇数也可能为偶数,因此小伙伴们在对指针移动的条件加以补充,这里采取的是快指针为空指针或或者快指针的下一个结点为空来作为循环结束的条件。具体代码如下:

  1. //结构体的定义
  2. /**
  3. * Definition for singly-linked list.
  4. * struct ListNode {
  5. * int val;
  6. * struct ListNode *next;
  7. * };
  8. */
  9. struct ListNode* middleNode(struct ListNode* head)
  10. {
  11. struct ListNode*fast=head; //快指针
  12. struct ListNode*slow=head; //慢指针
  13. while( fast && fast->next)
  14. {
  15. fast=fast->next->next;
  16. slow=slow->next;
  17. }
  18. return slow;
  19. }

 3.链表的倒数第k个结点(牛客)

        本题其实也可以采取“快慢指针”的思路,只不过是空间上的快慢,即设立两个指针,让其中一个指针先行移动 k-1步,继而两个指针同时移动,当先行移动的指针来到链表的最后一个结点时,后移动指针就来到了倒数第k个结点。

        不过本题要注意的一个特殊情况就是当k值大于链表的结点个数的时候,便会出现快指针已经来到链表的最后一个结点时,仍旧要往后移动,这时候就导致了对空指针的访问,所以我们要对快指针移动后的位置进行判断,当来到最后一个结点时就返回NULL指针。

  1. //结构体的定义
  2. /**
  3. * struct ListNode {
  4. *int val;
  5. *struct ListNode *next;
  6. * };
  7. */
  8. struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
  9. {
  10. struct ListNode*fast=pListHead;
  11. struct ListNode*slow=pListHead;
  12. //空链表
  13. if(pListHead==NULL)
  14. {
  15. return NULL;
  16. }
  17. while(--k)
  18. {
  19. fast=fast->next;
  20. //k大于链表的节点数
  21. if(fast==NULL)
  22. {
  23. return NULL;
  24. }
  25. }
  26. while(fast->next)
  27. {
  28. fast=fast->next;
  29. slow=slow->next;
  30. }
  31. return slow;
  32. }

四、结语

         到此为止,有关单链表剩余的功能实现以及链表相关题目的讲解已经结束了。

         关注我 _麦麦_分享更多干货:_麦麦_的博客_CSDN博客-领域博主
         大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!

文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览40346 人正在系统学习中