当前位置:   article > 正文

数据结构初阶之单链表(C语言实现)_从键盘输入一系列整数(不少于3个整数)存储在单链表中然后从键盘读入一个单独的整

从键盘输入一系列整数(不少于3个整数)存储在单链表中然后从键盘读入一个单独的整

虽然顺序具有方便下标随机访问(因为是连续物理空间)的优点,但是也具有一定的缺陷,

如:

1. 插入数据,空间不足时要扩容,但是扩容有性能消耗

2. 头部或者中间位置插入删除数据,需要挪动数据,效率比较低

3. 空间扩容太大,可能存在一定空间占用,浪费空间,不能按需申请和释放空间

由于这些缺陷,链表诞生了

那么链表是什么?

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链 接次序实现的 。

从上图可看出:

1. 链式结构在逻辑上是连续的,但是在物理上不一定连续

2. 现实中的结点一般都是从堆上申请出来的

3. 从堆上申请的空间,是按照一定的此略来分配的,两次申请的空间可能连续,也可能不连续

链表分类:

单向或者双向

 带头或者不带头

循环或者非循环

 常用的结构:

无头单向非循环链表

结构简单,一般不会单独用来存储数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在面试笔试中出现很多。

带头双向循环链表

结构最复杂,一般用在单独处处数据,实际中使用的链表数据结构,都是带头双向循环链表。另外整个结构虽然结构复杂,但是使用代码实现以后会发现会带来很多优势,实现反而简单了


 这里实现的是无头单向非循环链表

将要完成接口有:

  1. //创建结点
  2. SListNode* BuySListNode(SListDateType x);
  3. //打印链表
  4. void PrintSList(SListNode* phead);
  5. //尾插
  6. void SListPushBack(SListNode** pphead, SListDateType x);
  7. //头插
  8. void SListPushFornt(SListNode** pphead, SListDateType x);
  9. //尾删
  10. void SListPopBack(SListNode** pphead);
  11. //头删
  12. void SListPopFront(SListNode** pphead);
  13. //查找指定值
  14. SListNode* SListFind(SListNode* phead, SListDateType x);
  15. //在指定位置前插入
  16. void SListInsertBefore(SListNode** pphead, SListNode* pos, SListDateType x);
  17. //在指定位置后插入
  18. void SListInsertAfter(SListNode* pos, SListDateType x);
  19. //删除指定位置
  20. void SListErase(SListNode** pphead, SListNode* pos);
  21. //删除指定位置后结点
  22. void SListEraseAfter(SListNode* pos);
  23. //销毁链表
  24. void SListDestroy(SListNode** pphead);

注:无头单向非循环链表需要理解二级指针传参,以及链表为空和不存在与顺序表的为空和不存在

定义的结构体结点:

  1. typedef int SListDateType;//方便类型灵活变换
  2. typedef struct SListNode
  3. {
  4. int date;//数据
  5. struct SListNode* next;//存放下一个结点的地址
  6. }SListNode;

创建新结点:

这里需要注意,由于链表的特点,我们操作结构体指针,而不是操作结构体,这一点与顺序表不同,所以我们要做的不是初始化,而是要写一个创建新结点的接口函数,当结点创建好就把数据放入

  1. //创建新结点
  2. SListNode* BuySListNode(SListDateType x)
  3. {
  4. //开辟一块动态内存
  5. SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
  6. //开辟失败终止程序
  7. if (newnode == NULL)
  8. {
  9. printf("Fail\n");
  10. exit(-1);
  11. }
  12. else
  13. {
  14. newnode->date = x;//将x放入新开辟date
  15. newnode->next = NULL;//开辟空间后不知道指向哪,先置空
  16. }
  17. //将开辟空间返回
  18. return newnode;
  19. }

尾部插入结点:

这里需要注意

由于尾插可能会改变第一个结点(链表为空),既然第一个结点会被改变,就应该传地址,而传过来的应该是一个一级指针指向链表的第一个结点,所以这里应该用二级指针接收

尽管传过来的一级指针指向的链表可能为空(即这个一级指针被赋为空),这也只是说明链表为空,但是存在这个链表,但是pphead作为应该二级指针指向传过来的一级指针,所以pphead里存放了这个一级指针的地址,既然有这个一级指针,那么这个一级指针便有地址存放一级指针,所以pphead不可能为空,否则就不是链表为空,那是链表不存在了,这是非法的情况

当链表为空需要分开讨论,直接将新创建的结点赋值

其他情况如下:

  1. void SListPushBack(SListNode** pphead, SListDateType x)
  2. {
  3. assert(pphead);//pphead不能为空
  4. //传x并接收创建的结点
  5. SListNode* newnode = BuySListNode(x);
  6. //如果传的是NULL,说明是空链表,将开辟的空间直接赋给空的链表
  7. if (*pphead == NULL)
  8. {
  9. //当链表尾空,将第一次新开的结点赋值,则*pphead存放的则始终是第一个结点
  10. *pphead = newnode;
  11. }
  12. //如果传的不是NULL,说明不是空链表,原来的链表有结点
  13. else
  14. {
  15. //找尾巴,将头结点给找尾结点
  16. SListNode* tail = *pphead;
  17. //当tail中的next等于NULL,说明其就是结尾的结点
  18. while (tail->next != NULL)
  19. {
  20. //找尾结点,不断的将下一个结点的地址给tail,直到下一个结点的地址是NULL
  21. tail = tail->next;
  22. }
  23. //当tail->next==NULL,将新开的结点赋值,替换NULL
  24. tail->next = newnode;
  25. }
  26. }

打印链表:

由于是打印,第一个结点也不会被改变,所以采用传值,即传一级指针的值,就用一级指针接收

  1. void PrintSList(SListNode* phead)
  2. {
  3. SListNode* cur = phead;//将头结点赋值给现结点
  4. //使用现结点遍历,cur->next==NULL是链表结束的标志
  5. while (cur != NULL)
  6. {
  7. printf("%d->", cur->date);
  8. cur = cur->next;
  9. }
  10. printf("NULL\n");
  11. }

头部插入结点:

这里头部插入和尾部插入同理,

插入情况如下:

  1. void SListPushFornt(SListNode** pphead, SListDateType x)
  2. {
  3. assert(pphead);//pphead不能为空
  4. //接收创建的新结点
  5. SListNode* newnode = BuySListNode(x);
  6. //将首结点地址给新建结点的next
  7. newnode->next = *pphead;
  8. //将新建结点的地址放首结点
  9. *pphead = newnode;
  10. }

尾部删除结点:

尾删和前面的尾插同理,还要注意链表为空以及链表只有一个结点的两种特殊情况,得分开情况讨论,因为prev不能为空,所以分情况讨论

没有结点无法删除

只有一个结点则直接删除

 其他情况如下:

  1. void SListPopBack(SListNode** pphead)
  2. {
  3. assert(pphead);//链表必须存在
  4. //空链表直接返回
  5. if (*pphead == NULL)
  6. {
  7. return;
  8. }
  9. //链表只有一个结点,直接释放
  10. else if ((*pphead)->next == NULL)
  11. {
  12. free(*pphead);
  13. *pphead = NULL;
  14. }
  15. //链表大于一个结点
  16. else
  17. {
  18. //采用双指针做法
  19. SListNode* tail = *pphead;
  20. SListNode* prev = NULL;
  21. //找尾
  22. while (tail->next != NULL)
  23. {
  24. //保留前一个结点的地址
  25. prev = tail;
  26. tail = tail->next;
  27. }
  28. //释放尾结点并置空
  29. free(tail);
  30. tail = NULL;
  31. //前一个结点的next置空
  32. prev->next = NULL;
  33. /*
  34. 两次解引用的做法
  35. while (tail->next->next != NULL)
  36. {
  37. tail = tail->next;
  38. }
  39. free(tail->next);
  40. tail->next = NULL;
  41. */
  42. }
  43. }

头部删除结点:

与尾插同理,还要链表不能为空,为空无法删除

  1. void SListPopFront(SListNode** pphead)
  2. {
  3. assert(pphead);//链表必须存在
  4. //链表为空
  5. if (*pphead == NULL)
  6. {
  7. return;
  8. }
  9. //链表不为空
  10. else
  11. {
  12. //存放第二个结点的地址
  13. SListNode* next = (*pphead)->next;
  14. //释放第一个结点
  15. free(*pphead);
  16. //指向第二个结点
  17. *pphead = next;
  18. }
  19. }

查找指定的值:

由于是查找,不改第一个结点,所以传值,用一级指针接收

  1. SListNode* SListFind(SListNode* phead, SListDateType x)
  2. {
  3. //遍历链表的现指针
  4. SListNode* cur = phead;
  5. //查找x
  6. while (cur)
  7. {
  8. if (cur->date == x)
  9. {
  10. return cur;
  11. }
  12. cur = cur->next;
  13. }
  14. //找不到
  15. return NULL;
  16. }

在指定位置前插入:

 与尾插同理,第一个结点可能改变,而且待插位置不能为空,这里需要分情况讨论,

当插入的结点是第一个结点时,正好是头插,因为prev不能为空,所以单独讨论

当插入的结点不是第一个时:

  1. void SListInsertBefore(SListNode** pphead, SListNode* pos, SListDateType x)
  2. {
  3. assert(pphead);//链表必须存在
  4. assert(pos);//插入的位置不能为空
  5. //当插入的结点是第一个时
  6. if (pos == *pphead)
  7. {
  8. SListPushFornt(pphead, x);
  9. }
  10. //当插入的结点不是第一个时
  11. else
  12. {
  13. //从第一个结点开始遍历
  14. SListNode* prev = *pphead;
  15. //查找pos位置
  16. while (prev->next != pos)
  17. {
  18. prev = prev->next;
  19. }
  20. //创建新的结点
  21. SListNode* newnode = BuySListNode(x);
  22. //插入
  23. prev->next = newnode;
  24. newnode->next = pos;
  25. }
  26. }

在指定位置后插入:

这种情况第一个不可能被改变,所以传值就行,使用一级指针接收,以及待插位置不能为空

  1. void SListInsertAfter(SListNode* pos, SListDateType x)
  2. {
  3. assert(pos);//插入的位置不能为空
  4. //创建结点
  5. SListNode* newnode = BuySListNode(x);
  6. //插入
  7. newnode->next = pos->next;
  8. pos->next = newnode;
  9. }

删除指定位置的结点:

有可能删除第一个结点,二级指针接收,位置不能为空

需要分情况讨论:

当删除的结点是第一个结点时,也就是头删,需要单独讨论,因为prev不能为空

其他情况如下:

  1. void SListErase(SListNode** pphead, SListNode* pos)
  2. {
  3. assert(pphead);//链表必须存在
  4. assert(pos);//查找的位置不能为空
  5. //当删除的位置是第一个结点时
  6. if (*pphead == pos)
  7. {
  8. SListPopFront(pphead);
  9. }
  10. //当删除的位置是不是第一个结点时
  11. else
  12. {
  13. //从第一个结点开始遍历
  14. SListNode* prev = *pphead;
  15. //查找pos
  16. while (prev->next != pos)
  17. {
  18. prev = prev->next;
  19. }
  20. //删除结点
  21. prev->next = pos->next;
  22. free(pos);
  23. pos = NULL;
  24. }
  25. }

删除指定位置后的结点:

不可能删除第一个结点,所以传值,一级指针接收,位置不能为空

  1. void SListEraseAfter(SListNode* pos)
  2. {
  3. assert(pos);//pos不能为空
  4. //存放待删除的结点
  5. SListNode* next = pos->next;
  6. //pos不是最后一个结点,也就是next不能为空
  7. if (next)
  8. {
  9. //删除结点
  10. pos->next = next->next;
  11. free(next);
  12. next = NULL;
  13. }
  14. }

销毁链表:

第一个结点会被改变,传地址,二级指针接收

  1. void SListDestroy(SListNode** pphead)
  2. {
  3. assert(pphead);//链表得存在
  4. SListNode* cur = *pphead;
  5. while (cur)
  6. {
  7. //指向待删除的下一个结点
  8. SListNode* next = cur->next;
  9. //删除
  10. free(cur);
  11. //指向下一个结点,正好到了最后一个结点cur被置空
  12. cur = next;
  13. }
  14. }

由此可见,单链表结构:

适合头插头删

不适合尾部或者中间某个位置插入删除

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

闽ICP备14008679号