详解单链表

简介: 详解单链表

💕十载寒窗无人问,一举成名天下知💕

作者:Mylvzi

文章主要内容:详解单链表

引言:

 我们之前已经学习过顺序表,顺序表是一种线性的存储结构,它在内存中是连续存放的;我们不难发现,顺序表在管理数据时存在一些问题,如进行插入数据时需要挪动大量数据,异地扩容导致内存使用率低,存在大量内存碎片等等,那有没有一种存储方式可以实现“随用随开“的内存使用方式呢?存在,就是我们今天要学的-->链表

链表的基本知识:

 链表是一种管理内存的数据结构,和顺序表一样,都是一种线性表;

单链表(Single List Table)的表示:

(指针域中只有一个地址,所以叫做单链表)

链表与顺序表的区别:

1.链表和顺序表最大的差距在于空间开辟的方式:

链表是有多少就开辟多少(精打细算)

顺序表是直接给你一大片空间,让你使用,不够用了,再给你一大片空间(任性父母)

2.顺序表的空间在内存中是连续的,而链表的空间是一个一个独立的小空间,不连续

单链表的创建:

前提准备:

//创建结构体存储单链表的基本信息(数据域 + 指针域):
typedef int SLDataType;
//定义结点
typedef struct SListNode
{
  SLDataType data;//数据域
  struct SListNode* next;//指针域-->存放下一节点的地址
}SLTNode;//SLT-->single list table单链表

单链表的管理:

打印单链表:

逻辑:从头指针(phead)访问下一个结点,打印每个结点的数据域,再把下一个结点的地址给cur,一直到NULL;

//打印链表
void SLTPrint(SLTNode* phead);
//打印链表逻辑
void SLTPrint(SLTNode* phead)
{
  //assert(phead);err
  //断言不合理,因为phead可以是NULL,代表链表中无数据
  //断不断言,要看的传的数据是否合理
  SLTNode* cur = phead;//重新赋头指针
  //while(cur != NULL)
  while (cur)
  {
    printf("%d->", cur->data);
    cur = cur->next;
    //++cur err 结点的存储在内存中不是连续的
  }
  printf("NULL");
}

创建新结点:

逻辑:为新结点动态开辟内存空间,然后再对数据域指针域进行赋值

//创建一个新结点
SLTNode* BuySListNode(SLDataType x);
//创建一个新结点
SLTNode* BuySListNode(SLDataType x)//要存储的数据为x
{
  //为整个结点动态开辟空间
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)//判断是否开辟成功
  {
    perror("malloc");
    exit(-1);
  }
  //初始化
  newnode->data = x;
  newnode->next = NULL;
}

尾插:

//尾插-->结点的本质是一种结构体指针,改变结构体指针需要传递结构体指针的地址(二级指针)
void SLTPushBack(SLTNode** pphead, SLDataType x);
//尾插-->结点的本质是一种结构体指针,改变结构体指针需要传递结构体指针的地址(二级指针)
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
    assert(pphead);//pphead是plist的地址,永远不可能为空,所以要检查;
  //assert(*pphead);err  空链表可以尾插
  //创建一个新结点作尾插用
  SLTNode* newnode = BuySListNode(x);
  //进行尾插-->找到尾结点,使其next指向newnode
  if (*pphead == NULL)
  {
    //如果*phead本事就是NULL,则不存在NULL->next,直接将newnode赋给phead即可
    *pphead = newnode;
  }
  else
  {
    //寻找到尾结点
    SLTNode* tail = *pphead;
    while (tail->next != NULL)
    {
      tail = tail->next;
    }
    tail->next = newnode;
  }
}

头插:

//头插
void SLTPushFront(SLTNode** pphead, SLDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
    assert(pphead);
  SLTNode* newnode = BuySListNode(x);
  //头插-->注意顺序
  newnode->next = *pphead;
  *pphead = newnode;
    //  这里改变了头结点,所以要用二级指针
  //err
  //*pphead = newnode;
  //newnode->next = *pphead;
}

注意到:头插和尾插的参数都是二级指针,原因在于你改变的是结点,结点本身就是一个结构体指针,改变结构体指针就要传递结构体指针的地址,即二级指针;在顺序表中,我们改变的是一个一个结构体 ,所以传递的是结构体指针

由于形参是实参的临时拷贝,出了函数作用域后会被自动销毁,要改变值,就要传递值的地址!

头插,尾插都要检查pphead,但不用检查*pphead,因为NULL也能插入数据

尾删:

//尾删
void SLTPopBack(SLTNode** pphead);
//尾删
void SLTPopBack(SLTNode** pphead)
{
    assert(pphead);//pphead为空,不合理
  //1.*pphead == NULL  如果是NULL,属于非法删除
  assert(*pphead);
  //2.只有一个节点
  if ((*pphead)->next == NULL)//注意:由于存在优先级问题,*pphead需要添加括号
  {
    free(*pphead);
    *pphead = NULL;
  }
  else//不止一个结点
  {
    //第一种写法
    SLTNode* tail = *pphead;
    //while (tail->next->next != NULL)
    while (tail->next->next)//在倒数第二个结点停止(因为你需要删除最后一个结点)
    {
      tail = tail->next;
    }
    free(tail->next);
    tail->next = NULL;
  }
  第二种写法:双指针法
  //  SLTNode* tailPrev = NULL;//tail结点的上一个结点
  //  SLTNode* tail = *pphead;
  //  while (tail->next)
  //  {
  //    tailPrev = tail;
  //    tail = tail->next;
  //  }
  //  free(tail);
  //  tail = NULL;
  //  //tailPrev->next = NULL;
}

注意尾删有三种情况

头删:

//头删
void SLTPopFront(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead);
  //1.空
  assert(*pphead);
  //2.非空
  SLTNode* newhead = (*pphead)->next;//使第二个结点作为头结点
  free(*pphead);
  *pphead = newhead;//这里不是置空,而是置换新结点为头结点
}

寻找结点:

根据输入的值,找到对应的结点

//寻找数据为x的结点
SLTNode* SLTFind(SLTNode* phead, SLDataType x);
SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
  assert(phead);
  SLTNode* cur = phead;//遍历尽量多定义一个变量
  while (cur)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  //出循环,遍历到NULL
  return NULL;
}

在pos之前插入x:

逻辑:找到pos之前的prev结点,改变指针域

// 在pos之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
// 在pos之前插入x-->单链表前插效率不高
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
  assert(*pphead);
  //pos是*pphead-->头插
  if (pos == *pphead)
  {
    SLTPushFront(pphead, x);//头插函数的第一个参数是plist的地址,是二级指针
  }
  else
  {
    //找到pos之前的结点prev
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    SLTNode* newnode = BuySListNode(x);
    prev->next = newnode;
    newnode->next = pos;
  }
}

在pos之后插入x:

逻辑:找到pos,改变指针域(去观察什么数据发生了改变)

// 在pos以后插入x
void SLTInsertAfter(SLTNode* pos, SLDataType x);
// 在pos以后插入x
void SLTInsertAfter(SLTNode* pos, SLDataType x)
{
  assert(pos);//防止传错
  //插入逻辑
  SLTNode* newnode = BuySListNode(x);
  newnode->next = pos->next;
  pos->next = newnode;
}

删除pos位置的结点:

逻辑:找到prev,改变指针域,free(pos)

// 删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    assert(pphead);
  assert(*pphead);
  assert(pos);
  //头指针-->头删
  if (pos == *pphead)
  {
    SLTPopFront(pphead);
  }
  else
  {
    //找到prev
    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
    pos = NULL;
  }
}

删除pos的后一个位置的结点:

逻辑:找到posNext,改变指针域,free(posNext);

// 删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos);
// 删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos)
{
  assert(pos);
  assert(pos->next);//检查pos是否是尾节点
  SLTNode* posNext = pos->next;
  pos->next = posNext->next;
  free(posNext);
  posNext = NULL;
}

删除整个链表:

逻辑:依次free,最后置空

//删除链表
void SLTDestroy(SLTNode** pphead);
//删除链表
void SLTDestroy(SLTNode** pphead)
{
  assert(pphead);
  //逻辑:一个一个释放,同时要能找到下一个结点
  SLTNode* cur = *pphead;
  //循环删除
  while (cur)
  {
    SLTNode* next = cur->next;
    free(cur);//释放cur所指向的内容,但是cur本身还在,是一个指针
    next = next->next;
  }
  *pphead = NULL;//最后对头指针置空
}

补充:链表和顺序表的区别

关于缓存利用率:

总结:

 单链表是一种链式的线性表,是一种常见的数据结构,但其对数据的管理效率并不高(只有头插,头删的效率还可以),但常常出题考察,在leetcode上非常常见,此外,单链表还经常作为更复杂数据结构的子结构出现,所以还是要掌握好单链表的相关知识,以及他的创建管理!希望这篇文章对你有用,谢谢观看!

目录
相关文章
|
25天前
|
人工智能 监控 Java
构建定时 Agent,基于 Spring AI Alibaba 实现自主运行的人机协同智能 Agent
借助 Spring AI Alibaba 框架,开发者可快速实现定制化自动定时运行的 Agent,构建数据采集、智能分析到人工参与决策的全流程AI业务应用。
542 39
|
3月前
|
Java 编译器
Java 17 Switch表达式:更简洁、更强大的流程控制
Java 17 Switch表达式:更简洁、更强大的流程控制
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
397 4
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
383 7
|
存储 算法 C语言
C语言手撕实战代码_二叉排序树(二叉搜索树)_构建_删除_插入操作详解
这份二叉排序树习题集涵盖了二叉搜索树(BST)的基本操作,包括构建、查找、删除等核心功能。通过多个具体示例,如构建BST、查找节点所在层数、删除特定节点及查找小于某个关键字的所有节点等,帮助读者深入理解二叉排序树的工作原理与应用技巧。此外,还介绍了如何将一棵二叉树分解为两棵满足特定条件的BST,以及删除所有关键字小于指定值的节点等高级操作。每个题目均配有详细解释与代码实现,便于学习与实践。
396 3
|
算法 C语言 开发者
C语言手撕实战代码_单链表
本文档详细介绍了使用C语言实现单链表的各种基本操作和经典算法。内容涵盖单链表的构建、插入、查找、合并及特殊操作,如头插法和尾插法构建单链表、插入元素、查找倒数第m个节点、合并两个有序链表等。每部分均配有详细的代码示例和注释,帮助读者更好地理解和掌握单链表的编程技巧。此外,还提供了判断子链、查找公共后缀等进阶题目,适合初学者和有一定基础的开发者学习参考。
181 2
|
存储 算法 C语言
C语言手撕实战代码_循环单链表和循环双链表
本文档详细介绍了用C语言实现循环单链表和循环双链表的相关算法。包括循环单链表的建立、逆转、左移、拆分及合并等操作;以及双链表的建立、遍历、排序和循环双链表的重组。通过具体示例和代码片段,展示了每种算法的实现思路与步骤,帮助读者深入理解并掌握这些数据结构的基本操作方法。
340 2
|
存储 算法 C语言
C语言手撕实战代码_二叉树_构造二叉树_层序遍历二叉树_二叉树深度的超详细代码实现
这段代码和文本介绍了一系列二叉树相关的问题及其解决方案。其中包括根据前序和中序序列构建二叉树、通过层次遍历序列和中序序列创建二叉树、计算二叉树节点数量、叶子节点数量、度为1的节点数量、二叉树高度、特定节点子树深度、判断两棵树是否相似、将叶子节点链接成双向链表、计算算术表达式的值、判断是否为完全二叉树以及求二叉树的最大宽度等。每道题目均提供了详细的算法思路及相应的C/C++代码实现,帮助读者理解和掌握二叉树的基本操作与应用。
348 2
|
JavaScript Java Spring
SpringBoot 接口输出文件流 & Vue 下载文件流,获取 Header 中的文件名
SpringBoot 接口输出文件流 & Vue 下载文件流,获取 Header 中的文件名
742 0
|
Java Maven 容器
springBoot项目导入外部jar包
springBoot项目导入外部jar包
1083 4