当前位置:   article > 正文

【C语言】实现双向链表

【C语言】实现双向链表


目录

(一)头文件

(二) 功能实现

(1)初始化

 (2)打印链表

(3) 头插与头删

(4)尾插与尾删

(5)指定位置之后插入

(6)删除指定位置的数据

(7)链表的销毁


 

正文开始:

        在实际应用中,常用的双向链表是双向带头循环链表,本文考虑到实际应用,目的也是实现双向带头循环链表。

(一)头文件

        命名"List.h"

        本文不加解释的给出头文件,按照头文件来实现双向链表的基本功能:包括打印链表,链表的初始化,头插与头删,尾插与尾删,在指定位置之后插入数据,删除指定位置的数据,链表的销毁等共九个功能。

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. //链表的数据类型
  6. typedef int LTtype;
  7. //双向链表的节点
  8. typedef struct ListNode
  9. {
  10. LTtype data;
  11. struct ListNode* prev;
  12. struct ListNode* next;
  13. }LTNode;
  14. //双向链表带有哨兵位,插入数据之前先插入哨兵位
  15. //初始化》传入头节点
  16. void LTInit(LTNode** pphead);
  17. //初始化2>返回头节点
  18. LTNode* LtInit0();
  19. //销毁链表
  20. void LTDestory(LTNode** pphead);
  21. //尾插
  22. void LTtail_push(LTNode* phead,LTtype x);
  23. //头插
  24. //是在头节点之后插入,在头节点之前插入等价于尾插
  25. void LThead_push(LTNode* phead,LTtype x);
  26. //打印便于调试
  27. void Print(LTNode* phead);
  28. //头删
  29. void LTpophead(LTNode* phead);
  30. //尾删
  31. void LTpoptail(LTNode* phead);
  32. //查找
  33. //为了找到pos位置
  34. LTNode* LTfind(LTNode* phead, LTtype x);
  35. //在pos之后插入数据
  36. void LTinsert(LTNode* pos, LTtype x);
  37. //删除pos处的数据
  38. void LTerase(LTNode* pos);

(二) 功能实现

        带头相对于不带头有诸多优势:

带头与不带头链表:

        带头链表是指在链表的开头增加一个额外的节点,该节点不存储具体的数据,仅用于辅助操作链表。带头链表的带头节点可以简化链表的操作,提高代码的可读性和可维护性。

        意义:

        带头节点可以避免链表为空的特殊情况处理。在没有带头节点的链表中,如果链表为空,添加、删除等操作都需要额外的处理逻辑。而带头节点的链表中,带头节点一直存在,无论链表是否为空,操作逻辑都是一致的,可以减少代码的复杂性。

        局限:

        造成额外的空间浪费。

(1)初始化

         初始化有两种方法,具体实现如下:

传址初始化

        初始化传递链表的地址(二级指针【通过传址,将形参与实参建立联系】),初始化要插入头指针哨兵位(不保存有效的数据),便于简化代码逻辑。

  1. //双向链表带有哨兵位,插入数据之前先插入哨兵位
  2. void LTInit(LTNode** pphead)
  3. {
  4. *pphead = (LTNode*)malloc(sizeof(LTNode));
  5. if (*pphead == NULL)
  6. {
  7. perror("malloc");
  8. exit(1);
  9. }
  10. (*pphead)->data = -1;
  11. (*pphead)->next = (*pphead)->prev = *pphead;
  12. }

接受返回值初始化

        在函数内申请头节点,最后返回到主函数内:

  1. //初始化2>返回头节点
  2. LTNode* LtInit0()
  3. {
  4. LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
  5. if (phead == NULL)
  6. {
  7. perror("malloc");
  8. exit(1);
  9. }
  10. phead->data = -1;
  11. phead->next = phead->prev = phead;
  12. return phead;
  13. }

 (2)打印链表

        以便于调试,理解代码;

        与打印单链表一样,不同的是:while的跳出条件是 pcur != phead;

  1. //打印链表
  2. void Print(LTNode* phead)
  3. {
  4. assert(phead);
  5. LTNode* pcur = phead->next;
  6. while (pcur != phead)
  7. {
  8. printf("%d->", pcur->data);
  9. pcur = pcur->next;
  10. }
  11. printf("\n");
  12. }

(3) 头插与头删

        插入操作需要申请新节点:

  1. //获取新节点
  2. LTNode* LTbuynewnode(LTtype x)
  3. {
  4. LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
  5. if (newnode == NULL)
  6. {
  7. perror("malloc");
  8. exit(1);
  9. }
  10. newnode->data = x;
  11. newnode->next = newnode->prev = NULL;
  12. return newnode;
  13. }

 

        头插与头删

         头插,是在头节点之后插入,在头节点之前插入等价于尾插;

         先对新节点进行操作,因为新节点与原节点没有联系,不会因为对指针进行操作而丢失节点,造成数据丢失和内存泄漏;

        我们可以直接得到phead,因此先让phead的下一个节点的前驱指针指向newnode,再改变phead的next指针;(如果反过来,先改变phead的next指向newnode,此时newnode后面的节点就找不到了。)

  1. //头插,是在头节点之后插入,在头节点之前插入等价于尾插
  2. void LThead_push(LTNode* phead, LTtype x)
  3. {
  4. assert(phead);
  5. LTNode* newnode = LTbuynewnode(x);
  6. //先对新节点进行操作
  7. newnode->next = phead->next;
  8. newnode->prev = phead;
  9. phead->next->prev = newnode;
  10. phead->next = newnode;
  11. }

头删

        断言传过来的指针不为空,链表不为空,通过assert实现;

        通过创建del指针(要被删除的节点),保存旧头节点;

        通过创建next指针,保存旧头节点的next节点;

        这样命名后进行头删,逻辑清晰,不易出错;

  1. //头删
  2. void LTpophead(LTNode* phead)
  3. {
  4. assert(phead);
  5. assert(phead->next != phead);
  6. LTNode* del = phead->next;//被删除的节点
  7. LTNode* next = del->next;//del的后置节点
  8. phead->next = next;
  9. next->prev = phead;
  10. free(del);
  11. del = NULL;
  12. }

 

(4)尾插与尾删

尾插

        与头插的逻辑完全相同

  1. //尾插
  2. void LTtail_push(LTNode* phead, LTtype x)
  3. {
  4. assert(phead);
  5. LTNode* newnode = LTbuynewnode(x);
  6. newnode->next = phead;
  7. newnode->prev = phead->prev;
  8. phead->prev->next = newnode;
  9. phead->prev = newnode;
  10. }

尾删

        与头删的逻辑完全相同

  1. //尾删
  2. void LTpoptail(LTNode* phead)
  3. {
  4. assert(phead);
  5. assert(phead->next != phead);
  6. LTNode* del = phead->prev;//被删除的节点
  7. LTNode* prev = del->prev;//del的前置节点
  8. prev->next = phead;
  9. phead->prev = prev;
  10. free(del);
  11. del = NULL;
  12. }

 

(5)指定位置之后插入

查找

         查找数据值为x的节点,找到返回此节点,找不到返回NULL;

  1. //查找
  2. LTNode* LTfind(LTNode* phead,LTtype x)
  3. {
  4. assert(phead);
  5. LTNode* pcur = phead->next;
  6. while (pcur != phead)
  7. {
  8. if (pcur->data == x)
  9. {
  10. return pcur;
  11. }
  12. pcur = pcur->next;
  13. }
  14. return NULL;
  15. }

 

 在指定位置之后插入数据

        根据find找到的pos位置,申请新节点,插入到pos之后

  1. //在pos位置之后插入数据
  2. void LTinsert(LTNode* pos,LTtype x)
  3. {
  4. assert(pos);
  5. LTNode* newnode = LTbuynewnode(x);
  6. newnode->next = pos->next;
  7. newnode->prev = pos;
  8. pos->next->prev = newnode;
  9. pos->next = newnode;
  10. }

 

(6)删除指定位置的数据

         删除指定位置的数据,需要的指针有pos的前驱节点,pos节点。

  1. //删除pos处的数据
  2. void LTerase(LTNode* pos)
  3. {
  4. assert(pos);
  5. LTNode* prev = pos->prev;
  6. LTNode* next = pos->next;
  7. prev->next = next;
  8. next->prev = prev;
  9. free(pos);
  10. pos = NULL;
  11. }

 

(7)链表的销毁

        链表的销毁共有两种方法:

传址销毁

  1. //销毁链表
  2. void LTDestory(LTNode** pphead)
  3. {
  4. assert(pphead);
  5. //哨兵位不为空
  6. assert(*pphead);
  7. LTNode* pcur = (*pphead)->next;
  8. while (pcur != *pphead)
  9. {
  10. LTNode* next = pcur->next;
  11. free(pcur);
  12. pcur = next;
  13. }
  14. //释放哨兵位
  15. free(pphead);
  16. //此时可以改变哨兵位的
  17. pphead = NULL;
  18. }

 传值销毁

        一级指针phead传递的是值,不是phead的地址,所以在函数中改变phead并不会改变phead的实际值
        函数返回后,仍需要手动phead = NULL;

  1. //推荐使用一级,为了保持接口的一致性
  2. void LTDestory0(LTNode* phead)
  3. {
  4. assert(phead);
  5. LTNode* pcur = phead->next;
  6. while (pcur != phead)
  7. {
  8. LTNode* next = pcur->next;
  9. free(pcur);
  10. pcur = next;
  11. }
  12. free(phead);
  13. phead = NULL;//无效的置空,当做是习惯
  14. }
  15. //一级指针phead传递的是值,不是phead的地址,所以在函数中改变phead并不会改变phead的实际值
  16. //函数返回后,仍需要手动phead = NULL;

 

完~

未经作者同意禁止转载 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/90849
推荐阅读
相关标签
  

闽ICP备14008679号