【数据结构】——单链表实现

简介: 【数据结构】——单链表实现

前言        

       链表和顺序表都是线性表的一种,但是顺序表在物理结构和逻辑结构上都是连续的,但链表在逻辑结构上是连续的,而在物理结构上不一定连续;来看以下图片来认识链表与顺序表的差别

这里以动态顺序表为例,和链表中的单链表对比一下

动态顺序表

单链表

       这里就可以很清晰的看到顺序表的底层其实就是一个数组,数据的是连续存储的(顺序表物理结构连续);而链表它每一个数据都不是连续的(链表物理结构上不一定连续)。

链表节点

       通过观察上图,我们会发现链表每一个节点都存放在数据和下一个节点的地址。

       那么来想一下,为了链表每一个节点都统一起来,都存储有效数据和下一个节点的地址,我们就需要创建应该结构体,来存储有效数据和下一个节点的指针;
注:这里只是单链表

typedef int SLType;
typedef struct SLTNode
{
  SLType data;
  struct SLTNode* next;
}SLT;

创建好链表节点,接下来就来实习单链表存储数据的这些功能。

单链表实现

先来看一下单链表都实现都哪些功能

//输出链表
void SLTPrint(SLT* phead);
//创建节点
SLT* SLTCreat(SLType x);
//单链表头插
void SLTPushFront(SLT** pphead, SLType x);
//单链表尾插
void SLTPushBack(SLT** pphead, SLType x);
//单链表头删
void SLTPopFront(SLT** pphead);
//单链表尾删
void SLTPopBack(SLT** pphead);
//查找数据
SLT* SLTFind(SLT* phead, SLType x);
//指定位置之前插入
void SLTInsert(SLT** pphead, SLT* pos, SLType x);
//指定位置之后插入
void SLTInsertAfter(SLT* pos, SLType x);
//删除指定节点
void SLTErase(SLT** pphead, SLT* pos);
//删除指定位置后一个节点
void SLTEraseAfter(SLT* pos);

创建节点

       这里创建节点,还是使用动态内存来创建,创建完成后,将数据存储进去,并把新节点的下一个节点置为NULL

代码如下:

//创建节点
SLT* SLTCreat(SLType x)
{
  SLT* newnode = (SLT*)malloc(sizeof(SLT));
  assert(newnode);
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}

       测试一下代码是否正确

可以看到代码没有问题。

输出链表

       由于这里实现单链表,存储的是整型数据,就以整型的方式输出,若存储其他类型的数据,就以存储类型的方式输出。

输出链表,首先就要遍历链表,因为链表最后一个节点里存储的下一个节点的地址为空(即最后一个节点  ->next 为空)所以,遍历链表结束的条件就是ptail ==NULL,没输出一个就让ptail指向下一个节点,直到为空,遍历结束。

       来写代码实现:

//输出链表
void SLTPrint(SLT* phead)
{
  SLT* ptail = phead;
  while (ptail!= NULL)//也可以直接写成 ptail
  {
    printf("%d -> ", ptail->data);
    ptail = ptail->next;
  }
  printf("NULL\n");
}

这里先创建两个节点并存储数据输出看一下结果

int main()
{
  SLT* plist = SLTCreat(1);
  plist->next = SLTCreat(2);
  SLTPrint(plist);
  return 0;
}

       这里也成功输出插入的两个数据。(最后一个节点的next指向空)

单链表头插

       在链表头部插入数据,不用像顺序表那样去移动所以的有效数据,链表只需要改变一个指针的指向即可

假设现在链表中已经存在两个数据,再进行头插,这时就只需要改变plist的指向即可,当然新节点的next指针也要指向下一个节点(plist指向的节点)。

代码如下:

//单链表头插
void SLTPushFront(SLT** pphead, SLType x)
{
  assert(pphead);
  SLT* newnode = SLTCreat(x);
  newnode->next = *pphead;
  *pphead = newnode;
}

还有一种情况,如果现在链表中没有数据,再进行头插,这里代码也能实现链表没有数据时的头插

单链表尾插

       链表的尾插,因为指针传的是指向头节点的指针的地址,所以,我们需要先遍历链表,找到链表的尾部,再修改尾节点的next指针指向。

假设现在链表中已经存在两个数据,再进行尾插,此时我们只需要找到链表的尾部,修改尾节点next指针指向即可,代码如下

//单链表尾插
void SLTPushBack(SLT** pphead, SLType x)
{
  assert(pphead);
  SLT* newnode = SLTCreat(x);
  SLT* ptail = *pphead;
  //遍历链表
  while (ptail->next)
  {
    ptail = ptail->next;
  }
  ptail->next = newnode;
}

       考虑了这种情况,再来看以下链表为空的情况,如果链表为空,这里ptail->next就对空指针进行解引用操作了;所以,我们需要判断链表是否为空?如果为空,插入的新节点就是头节点,直接让plist(即*pphead)指向新节点即可;如果不为空就正常进行尾插。

最终代码如下:

//单链表尾插
void SLTPushBack(SLT** pphead, SLType x)
{ 
  assert(pphead);
  SLT* newnode = SLTCreat(x);
  if (*pphead == NULL) //判断链表是否为空
  {
    *pphead = newnode;
  }
  else {
    SLT* ptail = *pphead;
    //遍历链表
    while (ptail->next)
    {
      ptail = ptail->next;
    }
    ptail->next = newnode;
  }
}

这里代码可以正常进行尾插。

单链表头删

       链表头删,首先我们要判断链表是否为空,如果为空(空链表没有数据该如何删除呢

链表头删,我们需要修改plist(*pphead)指向链表的下一个节点即头节点的next指针。

       此外:因为我们的节点都是动态申请的内存,所以在删除时,需要把它释放掉。

思路很简单,写一下代码:

 

//单链表头删
void SLTPopFront(SLT** pphead)
{
  assert(pphead && *pphead);
  SLT* del = (*pphead);
  *pphead = (*pphead)->next;
  free(del);
  del = NULL;
}

再来看一个如果链表为空,又是啥结果呢?

现在链表已经为空,在执行一次头删代码

这里assert断言报错。

单链表尾删

       首先尾删与头删一样,链表不能为空。

       尾删与尾插也有共同之处,就是遍历链表,找到链表的尾节点。找到尾节点,删除尾节点;然后修改尾节点上一个节点的next指针为NULL;所以在遍历链表时就要多记录一个节点。

//单链表尾删
void SLTPopBack(SLT** pphead)
{
  assert(pphead && *pphead);
  SLT* ptail = *pphead;
  SLT* pcur = *pphead;
  //遍历链表
  while (ptail->next)
  {
    pcur = ptail;
    ptail = ptail->next;
  }
  pcur->next = NULL;
  free(ptail);
  ptail  = NULL;
}

       在测试的时候我们会发现一个问题,如果链表只有一个节点,删除之后我们的plist指针并没有置为空,而我们的空间已经释放掉了,这是很危险的 ;所以这里我们先判断一下链表是否只有一个节点,如果是,我们释放完空间以后,将(*pphead)置为空,以免出现野指针的情况。

完善后代码:

//单链表尾删
void SLTPopBack(SLT** pphead)
{
  assert(pphead && *pphead);
  if ((*pphead)->next== NULL)
  {
    free(*pphead);
    *pphead = NULL;
 
  }
  else {
    SLT* ptail = *pphead;
    SLT* pcur = *pphead;
    //遍历链表
    while (ptail->next)
    {
      pcur = ptail;
      ptail = ptail->next;
    }
    free(ptail);
    ptail = NULL;
    pcur->next = NULL;
  }
}

测试没有问题,代码能够完成尾删这个功能。

查找数据

       查找数据,遍历链表查找数据,如果找到就返回数据所在节点的地址,如果没有找到就返回NULL;

//查找数据
SLT* SLTFind(SLT* phead, SLType x)
{
  SLT* ptail = phead;
  while (ptail)
  {
    if (ptail->data == x)
    {
      return ptail;
    }
        ptail = ptail->next;
  }
  return NULL;
}

这里测试以下:

数据存在时

数据不存在时

指定节点之前插入

       在链表指定节点之前插入数据,我们还需要知道指定节点的前一个节点,所以仍然需要遍历链表,寻找指定节点pos前一个节点。

//指定位置之前插入
void SLTInsert(SLT** pphead, SLT* pos, SLType x)
{
  assert(pphead && *pphead);
  SLT* ptail = *pphead;
  if (ptail == pos)//头插
  {
    SLTPushFront(pphead, x);
  }
  else
  {
    SLT* newnode = SLTCreat(x);
    while (ptail->next != pos)//找到pos位置
    {
      ptail = ptail->next;
    }
    newnode->next = pos;
    ptail->next = newnode;
  }
}

当然,这里如果故意传NULL给pos,(这就没啥意义了)这里也可以写以下assert断言pos。

指定节点之后插入

       在指定节点之后插入数据,就不需要再进行遍历链表,这里直接插入即可。

//指定位置之后插入
void SLTInsertAfter(SLT* pos, SLType x)
{
  assert(pos);
  SLT* newnode = SLTCreat(x);
  newnode->next = pos->next;
  pos->next = newnode;
}

删除指定节点

       删除链表中的指定节点,我们需要这个节点的上一个节点,所以又需要遍历链表,找到pos上一个节点,修改pos->next指针指向。

代码如下:

//删除指定节点
void SLTErase(SLT** pphead, SLT* pos)
{
  //找到pos上一个节点
  SLT* ptail = *pphead;
  while (ptail->next != pos)
  {
    ptail = ptail->next;
  }
  SLT* p = pos->next;
  free(pos);
  pos = NULL;
  ptail->next = p;
}

删除指定节点后一个节点

       删除链表指定节点后一个节点,因为pos就是删除节点的上一个节点,所以不需要遍历链表,直接删除即可。

//删除指定位置后一个节点
void SLTEraseAfter(SLT* pos)
{
  assert(pos->next);
  SLT* del = pos->next;
  pos->next = pos->next->next;
  free(del);
  del = NULL;
}

这里如果传过来的就是链表的尾节点,那 删除后一个节点,所以断言pos->next;

代码总览

#include"SList.h"
//创建节点
SLT* SLTCreat(SLType x)
{
  SLT* newnode = (SLT*)malloc(sizeof(SLT));
  assert(newnode);
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}
//输出链表
void SLTPrint(SLT* phead)
{
  SLT* ptail = phead;
  while (ptail != NULL)//也可以直接写成 ptail
  {
    printf("%d -> ", ptail->data);
    ptail = ptail->next;
  }
  printf("NULL\n");
}
//单链表头插
void SLTPushFront(SLT** pphead, SLType x)
{
  assert(pphead);
  SLT* newnode = SLTCreat(x);
  newnode->next = *pphead;
  *pphead = newnode;
}
//单链表尾插
void SLTPushBack(SLT** pphead, SLType x)
{ 
  assert(pphead);
  SLT* newnode = SLTCreat(x);
  if (*pphead == NULL) //判断链表是否为空
  {
    *pphead = newnode;
  }
  else {
    SLT* ptail = *pphead;
    //遍历链表
    while (ptail->next)
    {
      ptail = ptail->next;
    }
    ptail->next = newnode;
  }
}
//单链表头删
void SLTPopFront(SLT** pphead)
{
  assert(pphead && *pphead);
  SLT* del = (*pphead);
  *pphead = (*pphead)->next;
  free(del);
  del = NULL;
}
//单链表尾删
void SLTPopBack(SLT** pphead)
{
  assert(pphead && *pphead);
  if ((*pphead)->next== NULL)
  {
    free(*pphead);
    *pphead = NULL;
 
  }
  else {
    SLT* ptail = *pphead;
    SLT* pcur = *pphead;
    //遍历链表
    while (ptail->next)
    {
      pcur = ptail;
      ptail = ptail->next;
    }
    free(ptail);
    ptail = NULL;
    pcur->next = NULL;
  }
}
//查找数据
SLT* SLTFind(SLT* phead, SLType x)
{
  SLT* ptail = phead;
  while (ptail)
  {
    if (ptail->data == x)
    {
      return ptail;
    }
    ptail = ptail->next;
  }
  return NULL;
}
//指定位置之前插入
void SLTInsert(SLT** pphead, SLT* pos, SLType x)
{
  assert(pphead && *pphead);
  SLT* ptail = *pphead;
  if (ptail == pos)//头插
  {
    SLTPushFront(pphead, x);
  }
  else
  {
    SLT* newnode = SLTCreat(x);
    while (ptail->next != pos)//找到pos位置
    {
      ptail = ptail->next;
    }
    newnode->next = pos;
    ptail->next = newnode;
  }
}
//指定位置之后插入
void SLTInsertAfter(SLT* pos, SLType x)
{
  assert(pos);
  SLT* newnode = SLTCreat(x);
  newnode->next = pos->next;
  pos->next = newnode;
}
//删除指定节点
void SLTErase(SLT** pphead, SLT* pos)
{
  //找到pos上一个节点
  SLT* ptail = *pphead;
  while (ptail->next != pos)
  {
    ptail = ptail->next;
  }
  SLT* p = pos->next;
  free(pos);
  pos = NULL;
  ptail->next = p;
}
//删除指定位置后一个节点
void SLTEraseAfter(SLT* pos)
{
  assert(pos->next);
  SLT* del = pos->next;
  pos->next = pos->next->next;
  free(del);
  del = NULL;
}

感谢各位大佬支持并指出问题,


相关文章
|
24天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
17天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
4天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
1天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
241 12
|
19天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
21天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2579 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
3天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
169 2
|
1天前
|
编译器 C#
C#多态概述:通过继承实现的不同对象调用相同的方法,表现出不同的行为
C#多态概述:通过继承实现的不同对象调用相同的方法,表现出不同的行为
101 65
|
21天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1578 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
4天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
256 2