当前位置:   article > 正文

数据结构【没头单链表】

数据结构【没头单链表】

目录

概念与结构

结点

链表的性质

链表的打印分析

实现单链表:

创建单链表数据

申请空间

尾插数据

打印

头插数据

尾删

头删

查询数据

指定位置前插入数据

指定位置后插入数据

删除pos节点

删除pos后面的节点

销毁

链表的分类

链表说明:


概念与结构

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

 这个单链表我们需要一个整行或其他类型的存放数据,还有一个结构体指针,结构体指针连接下一个节点。

结点

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/结点” 

结点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。

图中指针变量plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望 plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。 链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针 变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。

链表的性质

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续

2、结点⼀般是从堆上申请的

 3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:

假设当前保存的结点为整型:

  1. struct SListNode
  2. {
  3. int data; //节点数据
  4. struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
  5. };

当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数 据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。 当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在当前节点拿上下⼀个结点的地址就可以了。

链表的打印分析

给定的链表结构中,如何实现结点从头到尾的打印?

思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?

实现单链表:

创建3个文件,slist.h头文件,slist.c存放函数的文件,test.c测试文件


创建单链表数据

arr用来存放数据,p指向下一个节点

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<assert.h>
  4. typedef int data;
  5. typedef struct slist
  6. {
  7. //存放数据
  8. data* arr;
  9. //指向下一个节点
  10. struct slist* p;
  11. }SL;

在测试文件创建链表为NULL


申请空间

接收的是数值,返回的是空间,把arr赋值x,p给空返回add

  1. //申请空间
  2. SL* koj(data x)
  3. {
  4. //申请空间
  5. SL* add = (SL*)malloc(sizeof(SL));
  6. //判断是不是空
  7. if (add == NULL)
  8. {
  9. perror("malloc");
  10. exit(1);
  11. }
  12. //把数据赋值给add
  13. add->arr = x;
  14. add->p = NULL;
  15. //然后返回
  16. return add;
  17. }

尾插数据

头文件

使用二级指针接收,直接修改实参。用一级的话还要赋值。

声明尾插的函数,为什么要声明,因为如果我们有很多个函数的话要一个一个找很麻烦,所以声明在头文件,就像我们看书的目录一样方便我们查看有哪些函数。

  1. //尾插
  2. void weic(SL** r, data x);


add接收的新申请的空间

思路:先判断当前r是不是空,是就直接把申请的空间给过去就行了

有空间,把地址给tab循环走到最后一个节点,然后让最后一个节点的指针指向add空间

  1. //尾插
  2. void weic(SL** r, data x)
  3. {
  4. assert(r);
  5. //申请空间
  6. SL* add = koj(x);
  7. //判断单链表是不是空
  8. if (*r == NULL)
  9. {
  10. //是空就直接赋值
  11. *r = add;
  12. }
  13. //不是空就循环走到tab->p 的null位置,进行连接
  14. else
  15. {
  16. //把r地址给tab
  17. SL* tab = *r;
  18. while (tab->p)
  19. {
  20. //循环走到tab->p 的null位置,进行连接
  21. tab = tab->p;
  22. }
  23. //把申请的空间和tab->p连接
  24. tab->p = add;
  25. }
  26. }

打印

打印用一级就行了,我们只是打印数据。

  1. //打印
  2. void day(SL* r);

打印

循环往后打印,最后一个打印NULL

  1. //打印
  2. void day(SL* r)
  3. {
  4. SL* add = r;
  5. while (add)
  6. {
  7. printf("%d->", add->arr);
  8. add = add->p;
  9. }
  10. printf("NULL\n");
  11. }

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);

头插数据

  1. //头插
  2. void toc(SL** r, data x);


思路:申请add空间,add的指针指向头节点*r,把add给*r这样新申请的空间就是头节点了。

  1. //头插
  2. void toc(SL** r, data x)
  3. {
  4. assert(r);
  5. //申请空间
  6. SL* add = koj(x);
  7. //把申请空间的add->p指向r的当前地址
  8. add->p = *r;
  9. //把add的地址给r
  10. *r = add;
  11. }

  1. SL* add = NULL;
  2. //头插
  3. toc(&add, 1);
  4. toc(&add, 2);
  5. toc(&add, 3);
  6. toc(&add, 4);

尾删

  1. //尾删除
  2. void weisc(SL** r);


思路:判断当前节点的下一个节点是不是空,是就说明只有一个节点,直接释放。

循环往后走到最后一个节点,每走一步前保存到tab,就能拿到前一个节点了。

把tab->p赋值为空,释放add空间就可以了。

  1. //尾删除
  2. void weisc(SL** r)
  3. {
  4. assert(r && *r);
  5. //判断第一个节点的p是不是null,是就直接释放
  6. if ((*r)->p == NULL)
  7. {
  8. free(*r);
  9. *r = NULL;
  10. }
  11. else
  12. {
  13. //这个找后面的节点
  14. SL* add = *r;
  15. //这个是后面的前一个节点
  16. SL* tab = NULL;
  17. //循环到后面的节点
  18. while (add->p)
  19. {
  20. //把当前节点给tab
  21. tab = add;
  22. //然后指向下一个节点
  23. add = add->p;
  24. }
  25. //把tab赋值为null
  26. tab->p = NULL;
  27. //然后释放add的空间
  28. free(add);
  29. add = NULL;
  30. }
  31. }

我们可以看到尾插1,2,3,4删除了4这个节点

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);
  7. //尾删
  8. weisc(&add);
  9. day(add);

头删

  1. //头删除
  2. void tosc(SL** r);


思路:创建add保存头节点,r往后走一步,释放add空间

  1. //头删除
  2. void tosc(SL** r)
  3. {
  4. assert(*r);
  5. SL* add = *r;
  6. *r = (*r)->p;
  7. free(add);
  8. add = NULL;
  9. }

可以看到我们删了1和2。

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);
  7. //头删
  8. tosc(&add);
  9. tosc(&add);
  10. day(add);

查询数据

  1. //查询
  2. SL* cx(SL* r,data x);

思路:把r给add,让add循环判断每个节点的arr数据等不等于x,等于直接返回当前节点,不等于返回NULL

  1. //查询
  2. SL* cx(SL* r, data x)
  3. {
  4. SL* add = r;
  5. //循环查询
  6. while (add)
  7. {
  8. //判断是不是等于x
  9. if (add->arr == x)
  10. {
  11. //是就返回当前节点
  12. return add;
  13. }
  14. //指向下一个节点
  15. add = add->p;
  16. }
  17. //没有返回空
  18. return NULL;
  19. }

pos接收当前节点,等于空打印没找到,不等于打印找到了。

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);
  7. day(add);
  8. //查询
  9. SL* pos = cx(add, 4);
  10. if (pos == NULL)
  11. {
  12. printf("没找到\n");
  13. }
  14. else
  15. {
  16. printf("找到了\n");
  17. }

指定位置前插入数据

  1. //在指定位置前插入数据
  2. void zhidqcr(SL** r, SL* pos, data x);


思路:判断pos等于r就说明在第一个节点或只有一个节点,直接调用头插的函数就行了。

add循环到pos后面,tab->p连接pos,add->p连接tab。

  1. //在指定位置前插入数据
  2. void zhidqcr(SL** r,SL* pos, data x)
  3. {
  4. assert(r);
  5. assert(pos);
  6. //判断是不是在第一个节点
  7. if (pos == *r)
  8. {
  9. //是就调用头插
  10. toc(r,x);
  11. }
  12. else
  13. {
  14. //申请的空间
  15. SL* tab = koj(x);
  16. SL* add = *r;
  17. //循环到节点前面停下
  18. while (add->p != pos)
  19. {
  20. add = add->p;
  21. }
  22. //进行
  23. tab->p = pos;
  24. add->p = tab;
  25. }
  26. }

在3的前面插入了一个数据为99的节点

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);
  7. //查询
  8. SL* pos = cx(add, 3);
  9. //if (pos == NULL)
  10. //{
  11. // printf("没找到\n");
  12. //}
  13. //else
  14. //{
  15. // printf("找到了\n");
  16. //}
  17. zhidqcr(&add, pos, 99);
  18. day(add);

指定位置后插入数据

  1. //在指定位置后插入数据
  2. void zhidhcr(SL* pos, data x);


思路:add是新申请的空间,add->p连接pos下一个节点,pos->p连接add。

  1. //指定位置后插入数据
  2. void zhidhcr(SL* pos, data x)
  3. {
  4. assert(pos);
  5. //申请空间
  6. SL* add = koj(x);
  7. //把r后面那个节点的地址给新的节点
  8. add->p = pos->p;
  9. //把新的节点给r
  10. pos->p = add;
  11. }

我们可以看到在3后面插入了一个99的节点。

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);
  7. //查询
  8. SL* pos = cx(add, 3);
  9. //if (pos == NULL)
  10. //{
  11. // printf("没找到\n");
  12. //}
  13. //else
  14. //{
  15. // printf("找到了\n");
  16. //}
  17. zhidhcr(pos, 99);

删除pos节点

  1. //删除pos节点
  2. void scpos(SL** r, SL* pos);


思路:判断只有一个节点或等于当前节点,直接释放。

把r给add,让add循环到pos前一个节点,add->p指向pos下一个节点,然后释放pos空间。

  1. //删除pos节点
  2. void scpos(SL** r, SL* pos)
  3. {
  4. //我们需要pos前一个节点,和后一个节点连接
  5. assert(r && *r);
  6. assert(pos);
  7. //判断如果(第一个节点等于要删除的节点)直接释放
  8. if (*r == pos)
  9. {
  10. free(*r);
  11. *r = NULL;
  12. }
  13. else
  14. {
  15. SL* add = *r;
  16. //循环走到pos节点的后面
  17. while (add->p != pos)
  18. {
  19. add = add->p;
  20. }
  21. //把pos指向的节点,给pos前面的节点(把pos后面的节点和pos前面的节点进行连接)
  22. add->p = pos->p;
  23. //释放pos空间
  24. free(pos);
  25. pos = NULL;
  26. }
  27. }

我们可以看到3被删除了

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);
  7. //查询
  8. SL* pos = cx(add, 3);
  9. //if (pos == NULL)
  10. //{
  11. // printf("没找到\n");
  12. //}
  13. //else
  14. //{
  15. // printf("找到了\n");
  16. //}
  17. scpos(&add,pos);

删除pos后面的节点

  1. //删除pos后面的节点
  2. void schpos(SL* pos);


思路:把pos下一个节点给add,   pos->p指向add下一个节点。释放add空间。

  1. //删除pos后面的节点
  2. void schpos(SL* pos)
  3. {
  4. assert(pos&&pos->p);
  5. //把pos下一个节点给add
  6. SL* add = pos->p;
  7. //把add下个节点给pos
  8. pos->p = add->p;
  9. //释放add
  10. free(add);
  11. add = NULL;
  12. }

  1. SL* add = NULL;
  2. //尾插
  3. weic(&add, 1);
  4. weic(&add, 2);
  5. weic(&add, 3);
  6. weic(&add, 4);
  7. //查询
  8. SL* pos = cx(add, 3);
  9. //if (pos == NULL)
  10. //{
  11. // printf("没找到\n");
  12. //}
  13. //else
  14. //{
  15. // printf("找到了\n");
  16. //}
  17. scpos(&add,pos);
  18. day(add);

销毁

  1. //链表销毁
  2. void xiaoh(SL** r);

思路:add循环释放,tab保存add下一个节点,释放add,在把tab给add,

最后还剩下*r赋值为NULL。

  1. //链表销毁
  2. void xiaoh(SL** r)
  3. {
  4. assert(r && *r);
  5. //把r节点给add
  6. SL* add = *r;
  7. SL* tab = NULL;
  8. //循环走全部节点
  9. while (add)
  10. {
  11. //把add下一个节点给tab
  12. tab = add->p;
  13. //释放add节点
  14. free(add);
  15. //把tab给add
  16. add = tab;
  17. }
  18. //把一开始的*r赋值为NULL
  19. *r = NULL;
  20. }

释放完后还剩下*r的空间需要赋值为NULL


链表的分类

链表的结构⾮常多样,以下情况组合起来就有8种(2x2x2)链表结构:

链表说明:

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表和双向带头循环链表

1.⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦ 结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。 

2.带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头 双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实 现反⽽简单了,后⾯我们代码实现了就知道了。

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

闽ICP备14008679号