数据结构 | 链表的实现

article/2025/11/9 13:17:17

目录

  • 单链表
  • 双链表
  • 数组结构和链式结构的对比

线性表中,除了顺序表这一重要的结构,还有链式结构,而这也是我们常说的链表。

在这里插入图片描述

一般是通过定义结构体(类)的方式来表示链表的每一个结点。一般而言,链表的结点都会有数据域和地址域。数据域是用来存放数据的,而地址域是用来链接结点的。

常用的链表结构分为:

  1. 带头节点的和不带头结点的
  2. 单指针的和双指针的
  3. 循环的和不循环的

本次的链表结构的实现用C语言来实现。

代码也上传到Gitee

单链表

这次是实现的单链表是无头节点不循环的链表。

分文件实现:.h文件放的是相关函数的声明,.c文件是函数的定义。

.h文件

//无哨兵位结点单向不循环链表的实现#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//定义链表的数据类型
typedef int SLListDataType;//定义结构体的结点
typedef struct SLListNode {SLListDataType data;struct SLListNode* next;}SLLN;//动态申请一个结点
SLLN* BuySLListNode(SLListDataType x);//单链表的销毁
void SLListDestory(SLLN* plist);//单链表打印
void SLListPrint(SLLN* plist);//单链表尾插
void SLListPushBack(SLLN** plist, SLListDataType x);//单链表尾删
void SLListPopBack(SLLN** plist);//单链表头插
void SLListPushFront(SLLN** plist, SLListDataType x);//单链表头删
void SLListPopFront(SLLN** plist);//单链表查找某个数据
SLLN* SLListFind(SLLN* plist, SLListDataType x);//单链表在pos位置之前插入x,传入的pos应为结构体地址
void SLListInsert(SLLN** plist, SLLN* pos, SLListDataType x);//单链表在pos位置之后插入x
void SLListInsertAfter(SLLN* plist, SLLN* pos, SLListDataType x);//单链表删除在pos位置的值
//删除pos的位置,需要先得到该位置的上一个结点的位置
//这样我们才能把上一个结点和下一个结点连在一起
//所以一般是删除pos后面的那个结点
void SLListErase(SLLN** plist, SLLN* pos);void SLListEraseAfter(SLLN* plist, SLLN* pos);

typedef结点的数据类型,后续修改,函数中的实现不需要更改

定义结构体类型,包含数据域和指针域

结点需要动态申请,所以使用malloc函数

实现一个打印的函数,是为了方便检验函数是否实现正确

尾插函数是再链表的最后添加一个结点

尾删是把链表最后的结点删除

头插是在链表的头部加入一个新的结点,并且头指针指向新的结点

头删是把链表头部的结点删除,头指针指向下一个结点

查找数据是根据数据域的值来查找,当然也可以设计成根据具体的下标来查找

单链表在某一个位置插入一个结点,不需要想数组一样需要移动数据

.c文件

//无哨兵位头节点单向不循环链表的实现#include"SingleLinkedList.h"//动态申请一个结点并赋值
SLLN* BuySLListNode(SLListDataType x) {//开辟一个结点的空间SLLN* tmp = (SLLN*)malloc(sizeof(SLLN));//判断返回的指针是否为空if (tmp == NULL) {exit(-1);}else {//每次申请一个结点,该结点的放入需要存储的数据,next指针为空tmp->next = NULL;tmp->data = x;}return tmp;}//单链表的销毁
void SLListDestory(SLLN** plist) {assert(*plist && plist);SLLN* p = *plist;while (*plist != NULL) {p = *plist;*plist = p->next;free(p);}}//单链表打印
void SLListPrint(SLLN* plist) {//判断传入的指针是否为空//assert(plist);while (plist != NULL) {//打印格式要根据SLListDataType的类型来确定printf("%d->", plist->data);plist = plist->next;}printf("NULL\n");
}//单链表尾插
//这里传入的是结构体的二级指针,目的是为了修改外面的结构体指针,使其指向动态开辟的空间
void SLListPushBack(SLLN** plist, SLListDataType x) {//判断传入的二级指针是否为空assert(plist);SLLN* newnode = BuySLListNode(x);SLLN* p = *plist;if (*plist == NULL) {*plist = newnode;}else {while (p->next != NULL) {p = p->next;}p->next = newnode;}}//单链表尾删
//如果单链表只有一个结点,那么尾删后就没有结点了,我们就需要把plist置空
void SLListPopBack(SLLN** plist) {//判断传入的结构体指针是否为空,若为空则表示一个节点都没有,不需要再删除assert(*plist);//定义两个指针,用于记录最后的结点和倒数第二个的结点SLLN* tail = *plist;SLLN* prev = NULL;while (tail->next != NULL) {prev = tail;tail = tail->next;}//如果单链表中只有一个结点,那么我们直接free就可以了if (prev == NULL) {free(tail);*plist = NULL;}else {free(tail);prev->next = NULL;}}//单链表头插
//同理,这里也需要传入二级指针
void SLListPushFront(SLLN** plist, SLListDataType x) {SLLN* newnode = BuySLListNode(x);SLLN* p = *plist;if (*plist == NULL) {*plist = newnode;}else{newnode->next = p;*plist = newnode;}}//单链表头删
//需要改变头指针的指向,所以也需要传入二级指针
void SLListPopFront(SLLN** plist) {//判断是否至少有一个结点assert(*plist);SLLN* p = *plist;*plist = (*plist)->next;free(p);}//单链表查找某个数据
//如果找到了,返回指向该节点的地址
SLLN* SLListFind(SLLN* plist, SLListDataType x) {//判断传入的指针是否为空assert(plist);SLLN* p = plist;//当p为空指针时退出循环while (p) {if (p->data == x) {return p;}p = p->next;}return NULL;}//单链表在pos位置之后插入x
void SLListInsertAfter(SLLN* plist, SLLN* pos, SLListDataType x) {//判断传入的两个指针是否为空assert(plist && pos);SLLN* p = plist;while (p != pos) {//如果p为NULL,说明已经遍历完整个链表了,但是并没有找到合适的posif (p == NULL) {printf("输入的pos不存在\n");return;}p = p->next;}SLLN* newnode = BuySLListNode(x);newnode->next = p->next;p->next = newnode;}//单链表在pos位置之前插入x
//可能会出现头插的情况,因此这里也需要传入一个二级结构体指针
//此时需要定义两个结构体指针,一个指向pos另外一个指向pos前面的一个结点
void SLListInsert(SLLN** plist, SLLN* pos, SLListDataType x) {assert(plist && pos);SLLN* prev = NULL;SLLN* tail = *plist;SLLN* newnode = BuySLListNode(x);if (tail == pos) {//此时相当于头插,可以直接调用头插的接口newnode->next = *plist;*plist = newnode;}else {//这里tail != NULL 是防止遍历完链表后还没有找到pos 的情况//此处设计为如果遍历完链表后还没有找到pos那么就在最后插入该数据while (tail != pos && tail != NULL) {prev = tail;tail = tail->next;}prev->next = newnode;newnode->next = tail;}}//单链表删除在pos位置的值
//有可能出现头删的情况,因此也需要传入二级指针来改变头指针指向的头结点
void SLListErase(SLLN** plist, SLLN* pos) {assert(plist && pos);SLLN* prev = NULL;SLLN* tail = *plist;if (tail == pos) {*plist = tail->next;free(tail);}else {while (tail != pos) {//防止pos无效,使得遍历完整个链表后都没有找到posassert(tail);prev = tail;tail = tail->next;}prev->next = tail->next;free(tail);}}void SLListEraseAfter(SLLN* plist, SLLN* pos) {SLLN* p = plist;SLLN* tmp = NULL;while (p != pos) {//不允许传入无效的pos和删除最后一个结点之后的数据assert(p);p = p->next;}//不允许删除最后一个结点的后一个结点assert(p->next);tmp = p->next;p->next = tmp->next;free(tmp);}

需要注意的点:

  1. 尾删的时候,需要先遍历链表找到最后一个结点和最后一个结点的前一个位置,让前一个位置的结点指向NULL,然后释放最后一个结点
  2. 头插和头删的时候,会使链表第一个结点的地址发生改变,我们需要让外面的头指针相应的改变,要么使用传入二级指针的方式来进行修改,要么使用返回新的头节点的方式让外面的头指针接收
  3. 在链表中间插入一个结点的时候,注意断开旧的结点的顺序和链接新的结点的顺序,一定不可以让旧的结点先断开,不然的话就找不到被断开的后半部分了
  4. 单链表的增删改查不是很方便,所以一般不用作存储数据的结构,但是一般的题目考察的就是单链表,所以也是需要了解的

双链表

在这里插入图片描述

双链表指的是每一个结点都有两个指针,一个是后继指针用来指向下一个结点,一个是前驱指针用来指向前一个结点。

这里还是带有哨兵位的头结点的循环链表,头节点不用来存储数据,唯一作用就是next指针永远指向链表的真正的第一个结点。

.h文件

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int DataType;typedef struct ListNode {struct ListNode* prev;struct ListNode* next;DataType data;}ListNode;//创建头节点,并返回
ListNode* ListCreat();//链表节点的创建
ListNode* ListNodeBuy(DataType x);//链表的销毁
void ListDestroy(ListNode* pHead);//链表的打印
void ListPrint(ListNode* pHead);//链表的头插
void ListPushFront(ListNode* pHead, DataType x);//链表的头删
void ListPopFront(ListNode* pHead);//链表的尾插
void ListPushBack(ListNode* pHead,DataType x);//链表的尾删
void ListPopBack(ListNode* pHead);//链表的查找
ListNode* ListFind(ListNode* pHead, DataType);//在链表pos位置前面插入一个结点
void ListInsert(ListNode* pos, DataType x);//删除在pos位置上面的结点
void ListErase(ListNode* pos);

双链表的相关的函数定义在.h文件中,大概的功能跟单链表差不多

.c文件

//带头双向循环链表的实现
#include"List.h"//创建头节点,并返回
ListNode* ListCreat() {ListNode* pHead = (ListNode*)malloc(sizeof(ListNode));if (pHead == NULL) {exit(-1);}pHead->next = pHead;pHead->prev = pHead;return pHead;
}//链表节点的创建
ListNode* ListNodeBuy(DataType x) {ListNode* plist = malloc(sizeof(ListNode));if (plist == NULL) {exit(-1);}else {plist->data = x;plist->next = NULL;plist->prev = NULL;return plist;}}//链表的销毁
void ListDestroy(ListNode* pHead) {assert(pHead);assert(pHead->next);ListNode* cur = pHead->next;while (cur != pHead) {ListNode* next = cur->next;free(cur);cur = next;}free(pHead);}//链表的打印
void ListPrint(ListNode* pHead) {assert(pHead);ListNode* cur = pHead->next;while (cur != pHead) {printf("%d ", cur->data);cur = cur->next;}printf("NULL");printf("\n");
}//链表的头插
void ListPushFront(ListNode* pHead, DataType x) {ListNode* newnode = ListNodeBuy(x);pHead->next->prev = newnode;newnode->next = pHead->next;newnode->prev = pHead;pHead->next = newnode;}//链表的头删
void ListPopFront(ListNode* pHead) {assert(pHead->next != pHead);ListNode* tmp = pHead->next;ListNode* Nexttmp = tmp->next;pHead->next = tmp->next;Nexttmp->prev = pHead;free(tmp);
}//链表的尾插
void ListPushBack(ListNode* pHead,DataType x) {ListNode* newnode = ListNodeBuy(x);ListNode* tail = pHead->prev;newnode->next = pHead;newnode->prev = tail;tail->next = newnode;pHead->prev = newnode;}//链表的尾删
void ListPopBack(ListNode* pHead) {assert(pHead->prev != pHead);ListNode* tmp = pHead->prev;ListNode* Prevtmp = tmp->prev;	
}//链表的查找
ListNode* ListFind(ListNode* pHead, DataType x) {ListNode* cur = pHead->next;while (cur != pHead) {if (cur->data == x) {return cur;}else {cur = cur->next;}}return NULL;
}//在链表pos位置前面插入一个结点
void ListInsert(ListNode* pos, DataType x) {assert(pos);ListNode* newnode = ListNodeBuy(x);ListNode* posPrev = pos->prev;posPrev->next = newnode;newnode->prev = posPrev;newnode->next = pos;pos->prev = newnode;}//删除在pos位置上面的结点
void ListErase(ListNode* pos) {assert(pos);//不允许删除哨兵位的头节点assert(pos != pos->next);ListNode* Next = pos->next;ListNode* Prev = pos->prev;Prev->next = Next;Next->prev = Prev;free(pos);
}

需要注意的是:

  1. 由于该双链表能够找到前一个结点和后一个结点,在删除某一个结点的时候,不需要时刻记录着上一个结点
  2. 因为是循环链表,所以在删除最后的结点的时候,不需要遍历整个链表才能找到尾结点,只需要利用头节点的前驱指针就可以了
  3. 双向循环的链表一般用于存储数据

数组结构和链式结构的对比

数据结构和链式结构在逻辑结构上都是线性的

但是由于各自的特点,使用的场景也不同

数组的缺点:

  1. 空间不够了就需要扩容,扩容的消耗挺大的
  2. 在中间或者头部删除数据或者插入数据,需要移动数据,消耗大
  3. 一般为了避免频繁的扩容,每次扩容都是扩大为原来的2倍,但是这也可能造成空间的浪费

数组的优点:

  1. 支持随机访问,可以支持一些需要有序或者随机来访问的算法,如二分查找、快速排序等
  2. 相对于单链表,尾部删除数据和插入数据不需要遍历,效率较高

链表的缺点:

  1. 每一个结点除了存储数据还要存储指针,空间消耗较数组大
  2. 不支持随机访问,寻找某个数据需要遍历

链表的优点:

  1. 更合理的使用空间,需要存储的时候再申请空间,不会造成空间浪费
  2. 头部和中间删除数据不需要挪动数据

http://chatgpt.dhexx.cn/article/Qmy7QV96.shtml

相关文章

Java数据结构之链表

目录 一.单链表 1.单链表的介绍和内存布局 2.单链表的添加和遍历 3.单链表的插入 4.单链表的删除 二.双向链表 1.添加节点 2.遍历节点 3.插入节点 4.删除结点 5.测试 三.单向环形链表 1.问题的引出 ​编辑 2.构建环形链表 1.创建结点 3.测试 3.约瑟夫问题代码的…

c++数据结构:链表

链表是一种物理存储单元上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点&#xff08;链表中每一个元素称为结点&#xff09;组成&#xff0c;结点可以在运行时动态生成。每个结点包括两个部分&#xff1a;一个是…

java数据结构-链表详解

文章目录 1.数据结构-链表详解1.1单链表1.1.1单链表节点的尾部添加1.1.2单链表节点的自动排序添加1.1.3单链表节点的修改1.1.4单链表节点的删除 1.2单链表面试题1.2.1单链表的有效节点个数1.2.2单链表倒数第k个结点1.2.3单链表反转1.2.4单链表逆序打印 1.3双向链表1.3.1双向链表…

C语言数据结构链表(图文)

目录 一、链表的简单理解与引入 1.1 链表的引入 1.2 节点的理解 1.3 链表的分类 二、常用链表功能的实现 2.1 首先是节点的定义&#xff0c; 2.2 节点的创建 2.3 单链表的尾插 2.4 单链表的尾删 2.5 单链表的头插 2.6 链表的头删 2.7 单…

【数据结构】链表的学习总结

目录 1.链表的概念2.链表的结构1️⃣链表中单个结点的结构2️⃣链表的整体结构 3.链表的分类4.链表的实现1️⃣单向无头非循环2️⃣双向带头循环 1.链表的概念 链表&#xff0c;是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表 中的指针…

C++数据结构之链表(详解)

主要参考文章地址 01.链表基础知识 | 算法通关手册 (itcharge.cn)&#xff09; 本次内容是对链表的总结&#xff0c;可以看了上面的文章之后。 在看我下面的内容&#xff0c;做一个简短的复习&#xff0c;且本内容的代码均用C实现&#xff0c;而参考资料的代码则为python。 …

[数据结构]链表之单链表(详解)

文章目录 [数据结构]链表之单链表前言1.链表1.1链表的概念及结构1.2单链表与顺序表的区别与优缺点1.3八种链表类型、单向带头循环链表单向带头非循环链表单向不带头循环链表单向不带头非循环链表双向带头循环链表双向带头非循环链表双向不带头循环链表双向不带头非循环链表 2.单…

【数据结构与算法】详解什么是链表,并用代码手动实现一个链表结构

本系列文章【数据结构与算法】所有完整代码已上传 github&#xff0c;想要完整代码的小伙伴可以直接去那获取&#xff0c;可以的话欢迎点个Star哦~下面放上跳转链接 https://github.com/Lpyexplore/structureAndAlgorithm-JS 本文将来讲解一下一种常见的线性数据结构—链表&a…

数据结构-链表篇

数据结构中数组和链表是是使用频率最高的基础数据结构。数组作为数据存储结构有一定的缺陷。在无序数组中&#xff0c;搜索性能差&#xff0c;在有序数组中&#xff0c;插入效率又很低&#xff0c;而且这两种数组的删除效率都很低&#xff0c;并且数组在创建后&#xff0c;其大…

数据结构之——链表

目录 一、链表的概念及结构 二、单链表的实现&#xff08;无头单向非循环链表&#xff09; 1.单链表节点定义 2.单链表的接口实现 &#xff08;1&#xff09;动态申请一个节点 &#xff08;2&#xff09;单链表打印 &#xff08;3&#xff09;单链表的销毁 &#xff0…

【数据结构】链表

单链表 这张图是我们待会要实现的功能&#xff0c;我会尽可能的将每一步都说的很详细&#xff0c;方便理解。 链表的概念及结构 概念&#xff1a;链表是一种 物理存储结构上非连续 、非顺序的存储结构&#xff0c;数据元素的 逻辑顺序 是通过链表中的 指针链 接 次序实现的 。…

数据结构之链表

目录 一、链表的特点 二、虚拟头结点 三、链表的实现 1、定义LinkedList 2、 构造方法 3、基本方法 4、添加元素 5、查找元素 6、修改元素 7、删除元素 链表是一种物理存储单元上非连续、非顺序的数据结构。前几篇我们讲到的数组也好&#xff0c;基于数组实现的栈…

[数据结构] 链表(图文超详解讲解)

文章目录 一、链表是什么&#xff1f;二、链表 1.链表的结构2.链表方法的代码实现总结 一、链表是什么&#xff1f; 链表是一种物理存储结构上非连续存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。 二、链表 1.链表的结构 链表的结构如图: 链…

数据结构---单向链表,双向链表,单向环形链表

链表介绍 链表是以节点的方式来存储,是链式存储每个节点包含 data 域&#xff0c; next 域:指向下一个节点.如图:发现链表的各个节点不一定是连续存储.链表分带头节点的链表和没有头节点的链表&#xff0c;根据实际的需求来确定 修改节点功能 思路(1) 先找到该节点&#xff0c…

数据结构与算法——线性表(链表篇)

&#x1f60a;数据结构与算法——线性表&#xff08;链表篇&#xff09; &#x1f680;前言&#x1f680;线性链表&#xff08;单链表&#xff09;&#x1f6a2;概念&#x1f6a2;基本操作&#x1f47b;插入操作⛅按位序插入⛅指定结点的后插操作⛅指定节点的前插操作 &#x1…

什么是接口测试?为什么要做接口测试?

1. 什么是接口测试&#xff1f;为什么要做接口测试&#xff1f; 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互…

接口测试怎么测?

通过性验证&#xff1a;首先肯定要保证这个接口功能是好使的&#xff0c;也就是正常的通过性测试&#xff0c;按照接口文档上的参数&#xff0c;正常传入&#xff0c;是否可以返回正确的结果。 参数组合&#xff1a;现在有一个操作商品的接口&#xff0c;有个字段type&#xf…

接口测试—详细

目录 1.为什么要做接口测试 2.最简单的接口长什么样 3.入门级接口测试工具:postman的安装 4.Json简介 5.3A原则 6.unittest框架 7.requests库 8.第一个用例 9.什么是mock server 10.使用flask实现mock server 总结 1.为什么要做接口测试 很多同学反馈现在面试的时候…

什么是接口测试?测试人员为什么要做接口测试?

前言 我们都知道学习软件测试需要学习很多的东西&#xff0c;那么今天呢笔者想详细的和大家来唠唠接口自动化测试&#xff0c;当然了这篇文章笔者主要讲的是接口测试的理论基础&#xff0c;这都是笔者个人的一些观点整理&#xff0c;要是有什么 不对的地方欢迎大家留言指正哈。…

什么是接口测试?怎样做接口测试?

扫盲内容&#xff1a; 1.什么是接口&#xff1f; 2.接口都有哪些类型&#xff1f; 3.接口的本质是什么&#xff1f; 4.什么是接口测试&#xff1f; 5.问什么要做接口测试&#xff1f; 6.怎样做接口测试&#xff1f; 7.接口测测试点是什么&#xff1f; 8.接口测试都要掌…