基于结点的数据结构——链表(单链表&&双向循环链表)| 附完整源码 | 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


目录
相关文章
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
405 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
存储 算法 C语言
【C语言】深入浅出:C语言链表的全面解析
链表是一种重要的基础数据结构,适用于频繁的插入和删除操作。通过本篇详细讲解了单链表、双向链表和循环链表的概念和实现,以及各类常用操作的示例代码。掌握链表的使用对于理解更复杂的数据结构和算法具有重要意义。
4185 6
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
601 5
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
1819 4
|
存储
[数据结构] -- 单链表
[数据结构] -- 单链表
177 1
|
存储 Java
HashMap之链表转红黑树(树化 )-treefyBin方法源码解读(所有涉及到的方法均有详细解读,欢迎指正)
本文详细解析了Java HashMap中链表转红黑树的机制,包括树化条件(链表长度达8且数组长度≥64)及转换流程,确保高效处理大量数据。
1219 1
|
存储 缓存 C语言
C语言:链表和数组有什么区别
C语言中,链表和数组是两种常用的数据结构。数组是一种线性结构,元素在内存中连续存储,通过下标访问,适合随机访问且大小固定的情况。链表由一系列不连续的节点组成,每个节点存储数据和指向下一个节点的指针,适用于频繁插入和删除操作的场景,链表的大小可以动态变化。
1058 2
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
638 0
|
存储
数据结构(单链表)
数据结构(单链表)
199 0
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
438 59