• 单链表基础知识讲解


    顺序表的缺陷

    学了顺序表后我们会发现它有很多不方便的地方,也可以说是缺陷

    1、顺序表在进行头插或者中间位置插入时,需要挪动数据,此时的时间复杂度为O(N), 增加运行时间以及损耗。

    2、顺序表在增容时有一定的消耗:申请空间、拷贝数据、释放空间。

    3、增容可能造成一定的空间浪费(开辟100个字节空间,还需5个,这时又开辟了100个空间,造成浪费)。

    链表的结构

    为了解决上述问题,就有了链表的概念。

    链表就像是用链条一样将一个个结构体串联起来,不需要增容,插入数据也不需要挪动,在很多地方较好的解决了顺序表的问题。

    链表有8种结构:

     

     今天我们先从最基础的单链表讲起。

    单链表

    链表是依存结构体实现的一种数据结构,因此我们先在头文件中创建一个结构体。

    1. typedef int SLTDataType;
    2. typedef struct SListNode
    3. {
    4. SLTDataType data;
    5. struct SListNode* next;
    6. }SLTNode;

    单链表是一种非常简单的结构,因此可以不需要初始化,直接设定一个结构体头指针指向空就行了,这也叫空单链表。

    	SLTNode* plist = NULL;

    单链表的销毁

    虽然不用初始化,但是在使用完后需要销毁。

    1. void SListDestory(SLTNode* phead)//?????
    2. {
    3. }

    这里注意一点:

    一:传参的时候传的是 plist 还是 &plist,接收的时候是用一级指针*phead还是二级指针 *pphead

    一:应该传的是地址&plist,接收要用二级指针*pphead,因为plist的类型是SLTNode*类型的,是结构体指针,如果传plist本身,在函数使用改变的是在函数内的形参,对外部的实参plist没有影响

    举一个简单的例子:

    1. void swap(int* px, int* py)
    2. {
    3. int tmp = *px;
    4. *px = *py;
    5. *py = tmp;
    6. }
    7. int main()
    8. {
    9. int x = 1, y = 2;
    10. swap(&x, &y);
    11. return 0;
    12. }

    我们都知道交换x , y 需要传它们的地址,如果定义 int* px = &x, int* py =&y,这样传px,py,swap函数用一级指针接收还能完成交换吗?显然是不行的,这时就需要二级指针接收。

    1. void swap(int** ppx, int** ppy)
    2. {
    3. int* tmp = *ppx;
    4. *ppx = *ppy;
    5. *ppy = tmp;
    6. }
    7. int main()
    8. {
    9. int x = 1, y = 2;
    10. int* px = &x, * py = &y;
    11. swap(&px,&py);
    12. return 0;
    13. }

    所以,传一级指针要用二级指针接收,传结构体类型指针,要用相应的二级指针来接收。

    因此,这里要用**pphead。

    销毁代码如下:

    1. void SListDestory(SLTNode** pphead)
    2. {
    3. assert(pphead);
    4. SLTNode* cur = *pphead;
    5. while (cur)
    6. {
    7. SLTNode* next = cur->next;
    8. free(cur);
    9. cur = next;
    10. }
    11. pphead = NULL;
    12. }

     这里要多创建一个指针来保存当前free的节点的下一个节点,否则free过后就找不到下一个节点了。

    单链表的打印

    为了更好地理解单链表,我们先来写一个打印的接口。

    1. void SListPrint(SLTNode* phead)
    2. {
    3. assert(phead); //?????
    4. }

    这里因为不需要改变plist,所以传一级指针就可以了。

    注意:在这不能对phead断言。因为phead不可能是空。phead存的是plist的地址,就算plist == NULL,即链表为空,但plist的地址也不为空,因此,phead不可能为空。

    打印代码如下:

    1. void SListPrint(SLTNode* phead)
    2. {
    3. SLTNode* cur = phead;
    4. while (cur)
    5. {
    6. printf("%d->", cur->data);
    7. cur = cur->next;
    8. }
    9. printf("NULL\n");
    10. }

    单链表的头插尾插

    既然是插入数据,那么就需要保存新数据的新的节点,这里再写一个创建新节点的接口。

    1. SLTNode* BuySListNode(SLTDataType x)
    2. {
    3. SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTDataType));
    4. if (newnode == NULL)
    5. {
    6. perror("malloc fail");
    7. exit(-1);
    8. }
    9. newnode->data = x;
    10. newnode->next = NULL;
    11. return newnode;
    12. }

    然后创建新的节点头插

    1. void SListPushFront(SLTNode** pphead,SLTDataType x)
    2. {
    3. assert(pphead);
    4. SLTNode* newnode = BuySListNode(x);
    5. newnode->next = *pphead;
    6. *pphead = newnode;
    7. }

    逻辑角度分析:

     *pphead = NULL(链表为空时一样)

    物理角度分析:

    尾插:

    尾插有两种情况,要分开讨论:

     代码如下:

    1. void SListPushBack(SLTNode** pphead, SLTDataType x)
    2. {
    3. assert(pphead);
    4. SLTNode* newnode = BuySListNode(x);
    5. if (*pphead == NULL)
    6. {
    7. *pphead = newnode;
    8. }
    9. SLTNode* tail = *pphead;
    10. while (tail->next)
    11. {
    12. tail = tail->next;
    13. }
    14. tail->next = newnode;
    15. }

    注意这里找尾的时候容易出错,不是tail去去找,而是tail->next,tail->next = NULL时就找到了最后一个位置。

    头删尾删

    头删

     头删的时候注意要先记录第一个节点,然后将其free掉,否则*pphead指向第二个节点就找不到第一个节点了。

    1. void SListPopFront(SLTNode** pphead)
    2. {
    3. assert(pphead);
    4. assert(*pphead);
    5. SLTNode* del = *pphead;
    6. *pphead = (*pphead)->next;
    7. free(del);
    8. del = NULL;
    9. }

    尾删

     先找到尾,将其free是不是就行了?错!这是一个经典的野指针问题。

    free最后一个节点,倒数第二个节点还指向它,那就是野指针了。

    因此,可以用两个指针走,tail记录后一个,用prev记录前一个,tail找到最后一个时prev在倒数第二个的位置,将其next的指向改成null就行了。

     代码如下:

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

    当然,这里也有如果链表为空(*pphead == NULL)的情况,同样断言一下即可。

    还需注意的是还有一种只有一个节点的情况没有考虑,当只有一个元素时,会出现越界,所以要单独拿出来讨论一下。

    上述代码还有一种写法:

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

    查找和修改

    这个接口非常的简单,修改在查找到的基础上可以直接改。

    代码如下:

    1. SLNode* SListFind(SLNode* phead, SLTDataType x)
    2. {
    3. SLNode* cur = phead;
    4. while (cur)
    5. {
    6. if (cur->data == x)
    7. return cur;
    8. cur = cur->next;
    9. }
    10. return NULL;
    11. }
    12. //test.c
    13. SLNode* ret = SListFind(plist, 1);
    14. if (ret)
    15. {
    16. printf("找到了\n");
    17. //修改也可以实现
    18. ret->data *= 10;
    19. }
    20. else
    21. {
    22. printf("找不到\n");
    23. }

    指定位置插入

    这需要和上面的查找配合使用,找到需要插入的位置,再调用指定位置插入接口。

    指定位置前插(pos之前)

    1. //test.c
    2. SLNode* ret = SListFind(plist, 1);
    3. if (ret)
    4. {
    5. printf("找到了\n");
    6. //修改也可以实现
    7. ret->data *= 10;
    8. SListPosInsert(&plist, ret, 4);
    9. }
    10. else
    11. {
    12. printf("找不到\n");
    13. }
    14. SListPrint(plist);
    15. }
    16. //SList.c
    17. void SListPosInsert(SLNode** pphead, SLNode* pos,SLTDataType x)
    18. {
    19. assert(pphead);
    20. assert(pos);
    21. if (pos == *pphead)
    22. {
    23. SListPushFront(pphead, x);
    24. }
    25. else
    26. {
    27. SLNode* prev = *pphead;
    28. while (prev->next != pos)
    29. {
    30. prev = prev->next;
    31. //暴力检查,找不到pos,pos传错了
    32. assert(prev);
    33. }
    34. SLNode* newnode = BuySListNode(x);
    35. prev->next = newnode;
    36. newnode->next = pos;
    37. }
    38. }

     这里注意不能直接找到pos位置插入,因为单链表是单向的,不能后一个节点找前一个节点的位置,所以要创建一个prev指针去找pos前一个节点的位置。

    指定位置后插(pos之后)

    1. void SListPosInsertAfter(SLNode* pos, SLTDataType x)//pos位置之后
    2. {
    3. assert(pos);
    4. SLNode* newnode = BuySListNode(x);
    5. newnode->next = pos->next;
    6. pos->next = newnode;
    7. }

    后插非常简单,要注意的是连接节点的两句代码容易写反,要想清楚。

     

    指定位置删除

    指定位置处删除(删除pos处)

    分两种情况:pos在头上和不在头上。在头上调用头删接口就行了。

    不在头上,和指定位置插入一样,也要创建一个指针prev找到pos的前一个节点,连接prev和pos->next,最后free(pos)。注意:因为这里传参传的pos是一级指针,是无法改变外部实参的,所以要在Test函数中将pos置空。

    1. //SList.c
    2. void SListPosErase(SLNode** pphead, SLNode* pos)//pos位置之前
    3. {
    4. assert(pphead);
    5. assert(pos);
    6. if (pos == *pphead)
    7. {
    8. SListPopFront(pphead);
    9. }
    10. else
    11. {
    12. SLNode* prev = *pphead;
    13. while (prev->next != pos)
    14. {
    15. prev = prev->next;
    16. assert(prev);
    17. }
    18. prev->next = pos->next;
    19. free(pos);
    20. }
    21. }
    22. //Test.c
    23. void Test3()
    24. {
    25. SLNode* plist = NULL;
    26. SListPushFront(&plist, 1);
    27. SListPushFront(&plist, 2);
    28. SListPushFront(&plist, 3);
    29. SLNode* ret = SListFind(plist, 3);
    30. if (ret)
    31. {
    32. printf("找到了\n");
    33. SListPosErase(&plist, ret);
    34. ret = NULL;
    35. }
    36. else
    37. {
    38. printf("找不到\n");
    39. }
    40. }

    指定位置后部插入

    1. //SList.h
    2. void SListPosEraseAfter(SLNode* pos)//pos位置之后
    3. {
    4. assert(pos);
    5. if (pos->next == NULL)
    6. {
    7. return;
    8. }
    9. pos->next = pos->next->next;
    10. free(pos->next);
    11. }
    12. //Test.c
    13. void Test3( )
    14. {
    15. SListPrint(plist);
    16. SLNode* pos = SListFind(plist, 2);
    17. if (pos)
    18. {
    19. printf("找到了\n");
    20. SListPosEraseAfter(pos);
    21. }
    22. else
    23. {
    24. printf("找不到\n");
    25. }
    26. SListPrint(plist);
    27. SListDestory(&plist);
    28. }

    后删就很简单了,大家参考一下就行。

    总而言之,单链表因为结构原因,存在很多缺陷,不能完全解决顺序表的问题。只有头部操作的接口比较实用。以后我会讲一种带头双向循环链表,就能较好的解决以上问题了。

  • 相关阅读:
    在找工作时的准备工作:结合现状,针对意向企业做好充分准备
    Fourier傅里叶变换的线性性质和位移性质
    蓝桥杯刷题--python-16
    菜鸟学Kubernetes(K8s)系列——(三)关于Service、Ingress
    JZ70 矩形覆盖
    Java 内存模型
    运行时系统
    seismicunix基础-声波波动方程推导
    聊聊分布式架构01——http通信基础
    神经网络常用的训练方式,神经网络训练过程详解
  • 原文地址:https://blog.csdn.net/SAKURAjinx/article/details/126070761