基于结点的数据结构——链表(单链表&&双向循环链表)| 附完整源码 | C语言版(上)

简介: 基于结点的数据结构——链表(单链表&&双向循环链表)| 附完整源码 | C语言版(上)

0.png

目录


1.什么是链表

2.链表常见几种形式

3.无头单向非循环链表的实现

3.1结点结构的定义

3.2函数接口的实现

3.2.1尾插

3.2.2尾删

4. 带头双向循环链表的实现

4.1结点结构的定义

4.2函数接口的实现

5.两种链表的差异

①尾插与尾删的时间复杂度

②头插与头删的时间复杂度

③函数形参为何一个是二级指针,一个是一级指针?

完整源码

无头单向非循环链表

SList.h

SList.c

test.c

带头双向循环链表

List.h

List.c

test.c


正文


1.什么是链表


像数组一样,链表也用来表示一系列的元素。事实上,能用数组来做的事情,一般也可以用链表来做。然而,链表的实现跟数组是不一样的,在不同场景它们会有不同的性能表现。


计算机的内存就像一大堆格子,每格都可以用来保存比特形式的数据。当要创建数组时,程序会在内存中找出一组连续的空格子,给它们起个名字,以便你的应用存放数据。21.png

我们之前说过,计算机能够直接跳到数组的某一索引上。如果代码要求它读取索引 4的值,那么计算机只需一步就可以完成任务。重申一次,之所以能够这样,是因为程序事先知道了数组开头所在的内存地址——例如地址是 1000——当它想去索引 4时,便会自动跳到 1004处。


与数组不同的是,组成链表的格子不是连续的。它们可以分布在内存的各个地方。这种不相邻的格子,就叫作结点。


那么问题来了,计算机怎么知道这些分散的结点里,哪些属于这个链表,哪些属于其他链表呢?这就是链表的关键了:每个结点除了保存数据,它还保存着链表里的下一结点的内存地址。


这份用来指示下一结点的内存地址的额外数据,被称为链。链表如下图所示。  

22.png

注意:

1.从上图可以看出,链式结构在逻辑上是来连续的,但是在物理上不一定连续。

2.现实中的结点一般都是从堆上来申请的。

3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。


2.链表常见几种形式


①单向或者双向


78.png


②带头或者不带头

79.png

③循环或者非循环


80.png


虽然有这么多的链表的结构,但是我们实际中最常用到的还是这两种结构:


1. 无头单向非循环链表


结构简单,一般不会用来单独进行存储数据。实际中更多是作为其它数据结构的子结构,如哈希表、图的邻接表等等。另外这种结构在笔试面试中出现比较多。

88.png

2. 带头双向循环链表


结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后发现结构会带来很多优势,实现反而简单了。

0009.png


3.无头单向非循环链表的实现


就我个人而言,我认为虽然无头单向非循环链表是这几个链表中结构最简单的,但是实现却是最复杂的。我们学会了头单向非循环链表的实现,别的链表实现应当不在话下。

876.png


3.1结点结构的定义


typedef int SLTDataType;
typedef struct SListNode
{
  SLTDataType data;
  struct SListNode* next;
}SLTNode;

如上文所讲,一个结点包含的内容有数据data和保存下一个结点的地址的指针。


链表正式由一个个这样的结点串连起来的,所以我们只需要记住排在最前面的头结点的位置,就能访问链表里任意一个结点。


注意:无头单向非循环链表里的头结点指的是链表中的第一个结点,它本身也存储着有效数据。而后面所讲的带头双向循环链表中的头结点仅仅是作为链表起始的标志,并不会存储有效数据。


3.2函数接口的实现


//申请一个结点
SLTNode* BuySLTNode(SLTDataType data);
//创建一个链表,包含数据为0~n
SLTNode* CreateSList(int n);
//释放内存
void SLTDestroy(SLTNode** pphead);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType data);
//尾删
void SLTPopBack(SLTNode** pphead);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType data);
//头删
void SLTPopFront(SLTNode** pphead);
//查找data的结点
SLTNode* SLTFind(SLTNode** pphead, SLTDataType data);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType data);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType data);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//打印链表内容
void SLTPrint(SLTNode* phead);

由于代码量过大,为了避免影响阅读体验太差,我将完整代码置于文章末尾。此处,我们来细致的学习两个函数尾插与尾删,其它函数与其原理几乎相同。

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType data);
//尾删
void SLTPopBack(SLTNode** pphead);


3.2.1尾插


void SLTPushBack(SLTNode** pphead, SLTDataType data)
{
  SLTNode* newNode = BuySLTNode(data);
  //若为第一次插入,则分情况处理
  if (*pphead==NULL)
  {
    *pphead = newNode;
    return;
  }
  //找尾
  SLTNode* tail = *pphead;
  while(tail->next)
  {
    tail = tail->next;
  }
  //找到了,进行尾插
  tail->next = newNode;
}

首先将tail也指向头结点,通过变化tail来找到尾结点,如下图所示为找尾的过程:213.png

此时,tail所指向的结点的next为NULL,则循环结束,进行尾插。尾插只需要将tail所指向的结点的next指针赋值为newNode即可。

212.png


3.2.2尾删


void SLTPopBack(SLTNode** pphead)
{
  assert(pphead);
  //分情况处理
  if (*pphead == NULL)
  {
    free(*pphead);
    *pphead = NULL;
  }
  //找尾
  SLTNode* tail = *pphead;
  while (tail->next->next)
  {
    tail = tail->next;
  }
  //找到了,进行尾删
  free(tail->next);
  tail->next = NULL;
}

同样的尾删也是先找尾,但与尾插的找尾有一点不同的是while循环的判断条件不相同。原因是我们删掉尾结点后,还有重要的一步是将尾结点的前一个结点(也就是新的尾结点)的next置为NULL。所以这里的tail指向的是尾结点的前一个结点。

222.png此时tail->next->next 为NULL,所以循环结束,进行尾删。尾删只需要释放掉尾结点,并将新的尾结点的next置为NULL。

65.png


目录
相关文章
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
630 1
|
11月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
414 30
|
11月前
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
552 25
|
11月前
|
定位技术 C语言
c语言及数据结构实现简单贪吃蛇小游戏
c语言及数据结构实现简单贪吃蛇小游戏
|
12月前
|
搜索推荐 C语言
数据结构(C语言)之对归并排序的介绍与理解
归并排序是一种基于分治策略的排序算法,通过递归将数组不断分割为子数组,直到每个子数组仅剩一个元素,再逐步合并这些有序的子数组以得到最终的有序数组。递归版本中,每次分割区间为[left, mid]和[mid+1, right],确保每两个区间内数据有序后进行合并。非递归版本则通过逐步增加gap值(初始为1),先对单个元素排序,再逐步扩大到更大的区间进行合并,直至整个数组有序。归并排序的时间复杂度为O(n*logn),空间复杂度为O(n),且具有稳定性,适用于普通排序及大文件排序场景。
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
576 5
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
412 5
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
491 1
|
存储 SQL 算法
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表