单链表————单链表的构建,增删查改功能的实现

简介: 单链表————单链表的构建,增删查改功能的实现

1.什么是单链表

   单链表也是一种存储数据的结构之一,他和我们上一节讲到的顺序表有很大的去区别,上一节讲到的顺序表是由一个结构体做的框架,结构体里面有malloc开出的空间来存储数据,而今天的单链表是由数个结点构成的,每一个结点又都是一个小的结构体,因为每一个结构体里不仅有存储数据的空间,还有一个指针来存储下一个结点的地址,用来指向下一个结点。这样我们就可以发现单链表是由数个结点串起来。基于这样的结构,单链表是在内存空间里面是不连续的,但是在逻辑上是连续的。这也是单链表和顺序表最大的区别。后面我们将基于这样的结构特点,实现单链表的各种功能。


2.单链表的结构

单链表又一个个结点构成,结点又是一个结构体。结构体里面有两个结构体成员,第一个是一个存储数据的变量(data),第二个是一个指向下一个结构的结构体类型的指针(next),但是单链表的最后一个结点的next指向NULL,代表链表结束。代码示例:

typedef struct SlistNode
{
  SListdataType data;
  struct SlistNode* next;
}SlistNode;

这里使用typedef 是struct SlistNode 重命名为 SlistNode。


3.开辟一个新的结点

当我们有一个数据需要一个结点存储的时候,就需要开辟一个结点,结点也就是一个结构体,我们可以使用malloc开辟一个struct SlistNode结构体大小的空间作为一个结点,并将数据存入结点的data里面,因为新开辟的结点自然而然也就是单链表的最后一个结点,所以新开辟的结点的next需要指向空。代码实例:

SlistNode* BuySListNode(SListdataType x)//开辟结点;x 代表存储的数据;
{
  //增加结点的时候,结点是一个结构体,里面有一个结构体类型的指针,和一个数据存储变量,
  //所以申请空间的时候要申请一个节点的大小;
  SlistNode* newNode = (SlistNode*)malloc(sizeof(SlistNode));
  if (newNode == NULL)//判断是否开辟成功;
  {
    printf("开辟失败!");
    exit(-1);
  }
  newNode->data = x;//结点开辟成功后,将待存数据 x,存入data;
  newNode->next = NULL;//新增加的结点也是最后一个结点,需要将节点里面的结构体指针置空;
  return newNode;
}

这里如果开辟失败,后面也就没有必要对结构体操作的必要,直接exit(-1)暴力终止,最后我们返回新开辟节点的地址,因为需要将新开辟的节点的接到上一个结点的后面。


4.在链表的尾部插入一个节点数据

在链表的尾部插入一个节点数据,可以分为两步:(1)生成一个结点,并且将数据存入节点的data里面,将节点的next置空,因为新生成的结点也就是最后一个结点(2)将前一个结点里面的结构体指针指向新生成的结点的地址。这样就保持了我们单链表的逻辑上的连续性。


图片示例:



代码示例:

void SListpushback(SlistNode** pphead, SListdataType x)//尾插结点数据;
{
  SlistNode* newNode = BuySListNode(x);
  if (*pphead == NULL)//如果,*pphead为空,就意味着链表第一个结点就是空;直接增加一个结点
  {
    *pphead = BuySListNode(x);//将新增的节点的地址赋值给*pphead;
  }
  else//如果第一个结点不是空
  {
    SlistNode* tail = *pphead;
    while (tail->next != NULL)//找到最后一个结点;
    {
      tail = tail->next;
    }
    tail->next = newNode;//找到最后一个结点之后,将最后一个结点里面的结构体指针,赋值为新增加的节点的地址;
  }
}
int main()
{
  SlistNode* plist = NULL;
  SListpushback(&plist, 100);
  SListpushback(&plist, 200);
  SListpushback(&plist, 300);
  SListpushback(&plist, 400);
  SListpushback(&plist, 500);
  SListpushback(&plist, 600);
  SListpushback(&plist, 700);
    return 0;
}

这里调用的时候使穿过的时plist的地址,plist是一个结构体的指针,如果我们只是传的时plist,也就是简单的传值,虽然pliat本身是个地址,但是传递过去的是plist,plist最为一个值传递过去了,传值的调用是无法对链表发生变化的,所以这里是使用了&plist,作为函数的形参,就要用二级指针,来接收实参。也是为数不多的使用到二级指针的地方。


调用BuySListNode(x)函数返回的是新生成的节点的地址。如果,*pphead为空,就意味着链表第一个结点就是空;直接增加一个结点,将新增的节点的地址赋值给*pphead;

   单链表打印函数:单链表的遍历,只要循环的打印data的值,并且将结点往后移一下,看代码示例:

void SlistPrintf(SlistNode* phead)//打印函数
{
  SlistNode* cur = phead;//为了不在原链表上操作;我们重新用一个链表的头来操作;
  while (cur != NULL)//当cur==NULL时,链表结束。
  {
    printf("%d ", cur->data);
    cur = cur->next;//结点往后移;
  }
}

我们借助,打印函数,来看一下代码的效果,



5.删除链表尾部的结点

删除链表尾部的一个结点,其实很简单,我们只要找到最后一个结点,并且把它用free(),释放掉,再把倒数第二个结点的next置空。因为最后一个结点没有了以后,倒数第一个结点就是最后一个结点了。 但是有一个点需要注意,就是当我们找到最后一个结点的时候,就回不到倒数第二个结点里面的next,也就没办法给他置空了,所以为了能够在找到倒数第一个结点的时候还能找到倒数第二个,我们可以定义一前一后两个指针,后面的指针指到倒数第一个结点的时候,前面的指针就可以指向倒数第二个指针了。


图片示例:



代码示例:

void SListpophback(SlistNode** pphead)//尾删结点;
{
  if (*pphead == NULL)//说明一个结点都没有,无需操作;
  {
    return;
  }
  else if ((*pphead)->next == NULL)//就一个结点,直接把结点的空间地址置空;
  {
    free(*pphead);
    *pphead = NULL;
  }
  else//有多个结点;
  {
    SlistNode* prev = NULL;
    SlistNode* tail = *pphead;
    while (tail->next != NULL)//先找到最后一个结点; 
    {
      prev = tail;
      tail = tail->next;
    }
    free(tail);
    prev->next = NULL;//将最后一个结点的地址置空;
  }
}

如果链表中就一个结点,就不必考虑这么多了,直接 将这个结点free就可以了;

调用:

int main()
{
  SlistNode* plist = NULL;
  SListpushback(&plist, 100);
  SListpushback(&plist, 200);
  SListpushback(&plist, 300);
  SListpushback(&plist, 400);
  SListpushback(&plist, 500);
  SListpushback(&plist, 600);
  SListpushback(&plist, 700);
  printf("尾删前:");
  SlistPrintf(plist);
  printf("\n");
  printf("尾删后:");
  SListpophback(&plist);
    SlistPrintf(plist);
    return 0;
}

执行效果:



6.查找一个结点,以及改动一个结点

我们查找一个数据X,所在的结点,找到之后就直接返回结点的地址,如果没有就返回NULL;查询的方式很简单,接直接遍历链表,遍历的同时进行判断。



代码示例:

SlistNode* FindList(SlistNode* phead, SListdataType x)//查找一个结点;
{
  SlistNode* cur = phead;
  while (cur != NULL)
  {
    if ((cur->data) == x)//如果相等,就找到了,返回cur;
    {
      return cur;
    }
    else
    {
      cur = cur->next;//不相等就往后走,判断下一个;
    }
  }
  return NULL;//走到这里,链表都遍历完了,就是没找到,返回NULL;
}

这里代码方便演示,我们找到那个数据的结点地址,并且把它改动一下,调用的主函数:

int main()
{
  SlistNode* plist = NULL;
  SListpushback(&plist, 100);
  SListpushback(&plist, 200);
  SListpushback(&plist, 300);
  SListpushback(&plist, 400);
  SListpushback(&plist, 500);
  SListpushback(&plist, 600);
  SListpushback(&plist, 700);
  printf("改动前:");
  SlistPrintf(plist);
  printf("\n");
  SlistNode* pos= FindList(plist, 300);//获得数据所在结点的地址;
  if (pos != NULL)//找到数据的结点并且改动数据内容;
  {
    pos->data = 3000;
  }
  printf("改动后:");
  SlistPrintf(plist);
  return 0;
}

代码执行效果:


28ea78e0f715409f91fe1016644498b3.png


7. 在pos之后增加一个结点

在pos位之后,增加一个结点,首先我们结合查询函数,获得你想插入链表中的一个中间位置的地址,这个位置地址就是pos,我们申请一个新的结点(newNode),先将pos后一个结点的地址(pos->next)给新结点的next,再将新结点的地址给pos->next。这样新增加的结点就和我们的链表连接起来了。这里的先后顺序是不能变的,因为如果pos->next=newNode;后面也就找不到pos后面的一个结点。


 

void SlistInserAfter(SlistNode** pos, SListdataType x)//在pos之后增加一个结点;
{
  assert(*pos);
  SlistNode* newnode = BuySListNode(x);
  newnode->next = (*pos)->next;
  (* pos)->next = newnode;
}

函数调用:

int main()
{
  SlistNode* plist = NULL;
  SListpushback(&plist, 100);
  SListpushback(&plist, 200);
  SListpushback(&plist, 300);
  SListpushback(&plist, 400);
  SListpushback(&plist, 500);
  SListpushback(&plist, 600);
  SListpushback(&plist, 700);
  printf("插入前:");
  SlistPrintf(plist);
  printf("\n");
  SlistNode* pos= FindList(plist, 300);
  SlistInserAfter(&pos, 350);
  printf("插入后:");
  SlistPrintf(plist);
  return 0;
}

我们利用查询函数查询了300的结点位置地址,300的后面增加一个存入350的结点;

代码执行效果:



8.删除pos后面的一个结点

首先先保存一下,被删除结点的地址,在将pos的next指向被删除后面的结点的地址,最后释放掉被删除结点的空间,并将指针置空。



代码示例

void SlistEraseAfter(SlistNode** pos)//删除pos之后的一个结点;
{
  assert(*pos);
  SlistNode*Next=(*pos)->next;
  (*pos)->next = Next->next;
  free(Next);
  Next = NULL;
}

函数调用代码:

int main()
{
  SlistNode* plist = NULL;
  SListpushback(&plist, 100);
  SListpushback(&plist, 200);
  SListpushback(&plist, 300);
  SListpushback(&plist, 400);
  SListpushback(&plist, 500);
  SListpushback(&plist, 600);
  SListpushback(&plist, 700);
  printf("插入前:");
  SlistPrintf(plist);
  printf("\n");
  SlistNode* pos= FindList(plist, 300);
  SlistInserAfter(&pos, 350);
  printf("插入后:");
  SlistPrintf(plist);
  SlistEraseAfter(&pos);
  printf("\n");
  printf("删除后:");
  SlistPrintf(plist);
  return 0;
}


代码执行效果:


2cccc8293c76411694b0b28459309fc0.png


9.单链表的优缺点

单链表的优点:

1.链表是一个动态数据结构,因此它可以在运行时通过分配和取消分配内存来增长和收缩。 因此,无需给出链表的初始大小。


2.节点的插入和删除确实非常容易。 与数组不同,在插入或删除元素后,我们不必移动元素。 在链表中,我们只需要更新节点的下一个指针中存在的地址即可。


3.由于链表的大小可以在运行时增加或减小,因此不会浪费内存。 在数组的情况下,会浪费大量内存,例如,如果我们声明一个大小为10的数组并在其中仅存储6个元素,那么就会浪费4个元素的空间。 链表中没有这种问题,因为仅在需要时才分配内存。


单链表的缺点:

1.与数组相比,在链表中存储元素需要更多的内存。 因为在链表中,每个节点都包含一个指针,并且它本身需要额外的内存。


2.在链表中很难遍历元素或节点。 我们不能像按下标在数组中那样随机访问任何元素。 例如,如果我们要访问位置n处的节点,则必须遍历它之前的所有节点。 因此,访问节点所需的时间很大。


3.在链表中,反向遍历确实很困难。 在情况下,它双链表比较容易,但是向后指针需要额外的内存,因此浪费了内存。  


相关文章
|
2月前
|
存储
【单链表】数据结构单链表的实现
【单链表】数据结构单链表的实现
|
3月前
|
存储 缓存 算法
链表全景:探索单链表、双向链表与循环链表【实战演练】
链表全景:探索单链表、双向链表与循环链表【实战演练】
34 3
|
4月前
leetcode:203. 移除链表元素(有哨兵位的单链表和无哨兵位的单链表)
leetcode:203. 移除链表元素(有哨兵位的单链表和无哨兵位的单链表)
20 0
|
5月前
|
存储
数据结构:图文详解单链表的各种操作(头插法,尾插法,任意位置插入,删除节点,查询节点,求链表的长度,清空链表)
数据结构:图文详解单链表的各种操作(头插法,尾插法,任意位置插入,删除节点,查询节点,求链表的长度,清空链表)
272 0
|
1月前
|
缓存 算法 搜索推荐
【数据结构】链表(单链表与双链表实现+原理+源码)
【数据结构】链表(单链表与双链表实现+原理+源码)
|
3月前
|
存储 算法 Java
【数据结构与算法】4.自主实现单链表的增删查改
【数据结构与算法】4.自主实现单链表的增删查改
|
4月前
|
JavaScript 算法 前端开发
TypeScript算法专题 - blog2 - 单链表节点的索引、结点删除与链表反转
TypeScript算法专题 - blog2 - 单链表节点的索引、结点删除与链表反转
31 0
|
4月前
|
存储 编译器 测试技术
速学数据结构 | 手把手教你会单链表的构建方式
速学数据结构 | 手把手教你会单链表的构建方式
36 0
数据结构实验之链表七:单链表中重复元素的删除
数据结构实验之链表七:单链表中重复元素的删除
数据结构实验之链表五:单链表的拆分
数据结构实验之链表五:单链表的拆分