• 初阶数据结构学习记录——셋 单链表(1)


    目录

    SList.h

    SList.c

    test.c

    1、尾插

    2、尾删

    3、头部操作

    4、test.c


    顺序表中可以看出,实现增删等功能需要一个个挪动,并不简洁,比较繁琐。扩容时,需要整个进行扩容,如果是异地扩容,有一定代价,且可能存在一定空间浪费。

    针对这些问题的优化方案就是按需事情释放和不挪动数据。想使用再开辟一个,而要读取这些空间时,遍历并不行,用指针可快速访问,因此也就出现了链表。在链表中,每一块空间就是一个节点。具体以代码呈现。

    1. struct SListNode
    2. {
    3.     int data;
    4.     struct SListNode* next;
    5. }

    结构体里放一个指针,用来指向下一个节点.为了清楚地看到链表的结构。我先放上一小段代码。

    SList.h

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. typedef int SLTDataType;
    6. typedef struct SListNode
    7. {
    8. SLTDataType data;
    9. struct SListNode* next;
    10. }SLTNode;
    11. SLTNode* BuySLTNode(SLTDataType x);
    12. SLTNode* CreateSList(int n);
    13. void SLTPrint(SLTNode* phead);
    14. void SLTPushBack(SLTNode** pphead, SLTDataType x);
    15. void SLTPopBack(SLTNode** pphead);
    16. void SLTPushFront(SLTNode** pphead, SLTDataType x);
    17. void SLTPopFront(SLTNode** pphead);

    SList.c

    1. #include "SList.h"
    2. SLTNode* BuySLTNode(SLTDataType x)
    3. {
    4. SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    5. if (newnode == NULL)
    6. {
    7. perror("malloc fail");
    8. exit(-1);
    9. }
    10. newnode->data = x;
    11. newnode->next = NULL;
    12. return newnode;//这里返回一个指针的地址,自然要用一个指针来接收。
    13. }
    14. SLTNode* CreateSList(int n)
    15. {
    16. SLTNode* phead = NULL, * ptail = NULL;
    17. for (int i = 0; i < n; ++i)
    18. {
    19. SLTNode* newnode = BuySLTNode(i + 10);
    20. if (phead == NULL)
    21. {
    22. ptail = phead = newnode;
    23. }
    24. else
    25. {
    26. ptail->next = newnode;
    27. ptail = newnode;
    28. }
    29. }
    30. return phead;//其实phead也是局部变量,也被销毁了,但是在这之前已经传回去了。函数外负责接收的指针就能拿到整个链表的头位置地址,操作者也就可以使用这个链表了。
    31. }
    32. void SLTPrint(SLTNode* phead)
    33. {
    34. SLTNode* cur = phead;
    35. while (cur != NULL)
    36. {
    37. //printf("[%d|%p]->", cur->data, cur->next);
    38. printf("%d->", cur->data);
    39. cur = cur->next;
    40. }
    41. printf("NULL\n");
    42. }


     

    test.c

    1. #include "SList.h"
    2. void TestSList1()
    3. {
    4.     /*SLTNode* n1 = BuySLTNode(1);//为什么要用指针而不是SLTNode sl?因为整个链表每个节点我们需要保存好内容,如果第二种办法,开辟在栈区,那么出了函数数据就没有了,而创建指针,使用malloc,就会存储在堆区,自然也就符合要求了
    5.     SLTNode* n2 = BuySLTNode(2);
    6.     SLTNode* n3 = BuySLTNode(3);
    7.     SLTNode* n4 = BuySLTNode(4);
    8.     n1->next = n2;
    9.     n2->next = n3;
    10.     n3->next = n4;
    11.     n4->next = NULL;*/
    12. SLTNode* plist = CreateSList(10);
    13. SLTPrint(plist);
    14. }
    15. int main()
    16. {
    17. TestSList1();
    18. return 0;
    19. }

    链表有很多种,不过比较核心的就是无头单向非循环链表和带头双向循环链表。这篇文章写无头单向非循环链表。但本篇所写的代码一定是挫的,因为本篇主要在于理解单链表,之后再升级。

    刚才上头所写其实就是一个单链表。我们继续写它的插删等功能

    1、尾插

    事实上,单的尾部动作比较麻烦。尾插我们需要先找到尾,可是我们只有头,所以就需要一点点移过去,但是这样写是错误的

    1. SLTNode* newnode = BuySLTNode(x);
    2. SLTNode* tail = phead;
    3. while (tail != NULL)
    4. {
    5. tail = tail->next;
    6. }
    7. tail = newnode;

    为什么错误?tail->next是NULL的时候,tail被赋值为NULL,然后退出循环,把newnode给了tail,但是那个next并没有连接上newnode。这只是tail自己在改变。当退出整个函数时,tail这个局部变量就没了,newnode也没了,尾插并没有实现。

    1. while (tail->next != NULL)
    2. {
    3. tail = tail->next;
    4. }
    5. tail->next = newnode;

    所以我们需要控制好next和tail才能完成基本的单链表。现在在test.c做个测试

    1. void TestSList2()
    2. {
    3. SLTNode* plist = CreateSList(5);
    4. SLTPushBack(&plist, 100);
    5. SLTPushBack(&plist, 200);
    6. SLTPushBack(&plist, 300);
    7. SLTPrint(plist);
    8. }
    9. int main()
    10. {
    11. TestSList2();
    12. return 0;
    13. }

    输出结果很正确,是我们所想的。不过还有一个问题,如果头是个NULL呢?是NULL仍然可以继续运行,这不耽误什么,我们对尾插函数做一下改动即可

    1. void SLTPushBack(SLTNode* phead, SLTDataType x)
    2. {
    3. SLTNode* newnode = BuySLTNode(x);
    4. SLTNode* tail = phead;
    5. if (phead == NULL)
    6. {
    7. phead = newnode;
    8. }
    9. else
    10. {
    11. //找尾
    12. while (tail->next != NULL)
    13. {
    14. tail = tail->next;
    15. }
    16. tail->next = newnode;
    17. }
    18. }

    好,即使这样,phead = NULL开始执行后,打印出来的结果却是NULL。我插入的值呢?这个问题可能容易被忽略掉,plist是个指针,尾插的时候传过去的是plist,这个问题就和要交换两个数值,传过去的数值一样,要想改变plist这个指针的内容,就需要对指针的地址进行操作。所以phead应该是二级指针。

    1. void SLTPushBack(SLTNode** pphead, SLTDataType x)
    2. {
    3. SLTNode* newnode = BuySLTNode(x);
    4. if (*pphead == NULL)
    5. {
    6. *pphead = newnode;
    7. }
    8. else
    9. {
    10. SLTNode* tail = *pphead;
    11. //找尾
    12. while (tail->next)
    13. {
    14. tail = tail->next;
    15. }
    16. tail->next = newnode;
    17. }
    18. }

    2、尾删

    1. void SLTPopBack(SLTNode** phead)
    2. {
    3. SLTNode* tail = *phead;
    4. while (tail->next)
    5. {
    6. tail = tail->next;
    7. }
    8. free(tail);
    9. tail = NULL;
    10. }

    一段错误的代码。tail一直往后走,走到最后一个时,tail出循环,然后free,再出函数。tai是一个局部变量,出了函数就消失了,但是还在函数内时就已经释放最后指向的空间了。假设插入100,200,300。300所在的空间被释放了,看似已经删除,但是200的next并没有变成NULL,而是还指向第三块空间,这时候它就变成野指针了,因为没有我们没有写改变第二个next的代码。解决方案有两个,一个是双指针走,这样就能有另一个指针指向第二个。或者,while判断里tail->next->next,对后后个进行判断,tail停下来的时候就能停在第二个了。下面是第一个写法.

    1. void SLTPopBack(SLTNode** phead)
    2. {
    3. SLTNode* tail = *phead;
    4. SLTNode* prev = NULL;
    5. while (tail->next)
    6. {
    7. prev = tail;
    8. tail = tail->next;
    9. }
    10. free(tail);
    11. prev->next = NULL;
    12. tail = NULL;

    一次次删除后,又出现了问题,删到还剩最后一个时,这个函数貌似没法正常完成,需要单独处理这个情况,所以

    1. void SLTPopBack(SLTNode** pphead)
    2. {
    3. assert(*pphead);
    4. if ((*pphead)->next == NULL)
    5. {
    6. free(*pphead);
    7. *pphead = NULL;
    8. }
    9. else
    10. {
    11. SLTNode* tail = *pphead;
    12. SLTNode* prev = NULL;
    13. while (tail->next)
    14. {
    15. prev = tail;
    16. tail = tail->next;
    17. }
    18. free(tail);
    19. prev->next = NULL;
    20. }
    21. }

    现在这个尾删没啥问题了。但是呢,总还有失误的地方。如果没记清自己插入了多少数据,删干净后又来了一次,编译器可就难受了。为了应对这个情况,我们断言一下即可assert(*pphead),不过尾插就不需要了。

    第二个写法:

    1. void SLTPopBack(SLTNode** pphead)
    2. {
    3. assert(*pphead);
    4. if ((*pphead)->next == NULL)
    5. {
    6. free(*pphead);
    7. *pphead = NULL;
    8. }
    9. else
    10. {
    11. SLTNode* tail = *pphead;
    12. while (tail->next->next)
    13. {
    14. tail = tail->next;
    15. }
    16. free(tail->next);
    17. tail->next = NULL;
    18. }
    19. }

    3、头部操作

    链表的头部操作很简单。

    1. void SLTPushFront(SLTNode** pphead, SLTDataType x)
    2. {
    3. SLTNode* newnode = BuySLTNode(x);
    4. newnode->next = *pphead;//即使为空,也不需要单独处理
    5. *pphead = newnode;//pphead指向新的头;
    6. }
    7. void SLTPopFront(SLTNode** pphead)
    8. {
    9. assert(*pphead);
    10. SLTNode* next = (*pphead)->next;
    11. free(*pphead);
    12. *pphead = next;//只剩一个时也不需要单独处理
    13. }

    头插和头删功能很简单。这也是链表的优势所在,链表的尾部操作其实比较繁琐。

    4、test.c

    1. void TestSList3()
    2. {
    3. SLTNode* plist = NULL;
    4. SLTPushBack(&plist, 100);
    5. SLTPushBack(&plist, 200);
    6. SLTPushBack(&plist, 300);
    7. SLTPrint(plist);
    8. SLTPopBack(&plist);
    9. SLTPrint(plist);
    10. SLTPopBack(&plist);
    11. SLTPrint(plist);
    12. SLTPopBack(&plist);
    13. SLTPrint(plist);
    14. //SLTPopBack(&plist);
    15. //SLTPrint(plist);
    16. }
    17. void TestSList4()
    18. {
    19. SLTNode* plist = NULL;
    20. SLTPushFront(&plist, 100);
    21. SLTPushFront(&plist, 200);
    22. SLTPushFront(&plist, 300);
    23. SLTPushFront(&plist, 400);
    24. SLTPrint(plist);
    25. SLTPopFront(&plist);
    26. SLTPrint(plist);
    27. SLTPopFront(&plist);
    28. SLTPrint(plist);
    29. SLTPopFront(&plist);
    30. SLTPrint(plist);
    31. SLTPopFront(&plist);
    32. SLTPrint(plist);
    33. }
    34. int main()
    35. {
    36. TestSList4();
    37. return 0;
    38. }

    关于pos位置的操作下一篇再写。

    结束。

  • 相关阅读:
    ROS 笔记(03)— ROS 命令行工具(rostopic、rosservice、rosnode、rosmsg、rosparam、rossrv)
    UNet网络模型学习总结
    20221127今天的世界发生了什么
    Kafka 分区
    基于JAVA国外摇滚乐队交流和周边售卖系统计算机毕业设计源码+数据库+lw文档+系统+部署
    JVM虚拟机字节码执行引擎——类文件和类加载之前必看
    Java基础进阶-序列化
    频谱和功率谱的区别与联系
    LabVIEW步进电机的串口控制方法与实现
    非零基础自学Java (老师:韩顺平) 第5章 程序控制结构 5.5 嵌套分支 && 5.6 switch分支结构
  • 原文地址:https://blog.csdn.net/kongqizyd146/article/details/127645066