跳到主要内容
数据结构:顺序表与链表详解 | 极客日志
C 算法
数据结构:顺序表与链表详解 综述由AI生成 顺序表和链表两种数据结构。顺序表基于数组实现,通过首地址访问空间。链表分为单向、双向及循环链表,具有插入删除效率高但访问不便的特点。文章涵盖了链表的创建、头插尾插、遍历、删除、查询、销毁等基本操作,以及寻找中间节点、排序、判断环等复杂操作。最后对比了数组与链表在存储空间、元素个数、增删效率及访问便利性上的区别,并提供了相关 C 语言代码示例。
性能调优 发布于 2026/3/27 更新于 2026/6/2 31 浏览顺序表
本质等同于数组,通过申请堆区空间存储数据,通过首地址完成对所有空间的访问。
动态数组
根据元素个数确定数组大小。
链表
预备知识(链式存储)
概念 :由两种区域组成,数据区存放节点的数据,指针区存放相邻节点的地址的一种存储方式。
特点 :存储地址不连续。
优点 :插入删除效率高,可以利用小空间(空间利用率高)。
缺点 :访问元素不方便,增加额外空间(指针区)的开销。
分类
链表类型 节点结构 单向链表 当前节点数据内容、下一节点地址 双向链表 上一节点地址、当前节点数据内容、下一节点地址 双向循环链表 尾节点地址、头节点数据内容、下一节点地址(循环连接)
单向链表
链表操作传参为头节点的地址。
有头链表 :无需改变头指针。
无头链表 :需改变指针指向,故传入地址的指针(二级指针)。
链表的定义
链表可以不设定元素上限,可以嵌套定义,因为指针字节大小固定。
typedef int DataType;
typedef struct node {
DataType data;
struct node *pNextnode ;
} LinkNode;
链表的操作 操作链表需要传入的参数为链表地址(即头节点的地址)。链表操作的函数在头文件定义部分声明为外部函数,在调用头文件时可直接调用。
简单操作 操作小结:创建、插入(头/尾)、移除、遍历、更改、查询、销毁。
1. 链表的生成
定义一个头节点的指针,申请头节点空间(插入必备)。
在头节点的指针区存放 NULL 地址。
LinkNode *CreateEmptyLinkList (void ) {
LinkNode *pHeadNode = NULL ;
pHeadNode = malloc (sizeof (LinkNode));
if (NULL == pHeadNode) {
perror("fail to malloc" );
return NULL ;
}
pHeadNode->pNextnode = NULL ;
return pHeadNode;
}
2. 节点的插入(头插入)
定义一个插入节点指针,申请插入节点的空间(插入必备)。
在该插入节点的数据区存放插入数据,在该插入节点的指针区存放原链表首位有效节点的地址。
在头节点的指针区存放该插入节点的地址。
int HeadInsertNode (LinkNode *pHeadNode, DataType tmpData) {
LinkNode *pInsertNode = NULL ;
pInsertNode = malloc (sizeof (LinkNode));
if (pInsertNode == NULL ) {
perror("fail to malloc" );
return -1 ;
}
pInsertNode->data = tmpData;
pInsertNode->pNextnode = pHeadNode->pNextnode;
pHeadNode->pNextnode = pInsertNode;
return 0 ;
}
3. 节点的插入(尾插入)
定义一个插入节点指针,申请节点空间(插入必备)。
在该节点的数据区存放插入数据,在该节点的指针区存放 NULL 地址。
定义一个流动指针,遍历找到原链表的末尾节点。
在原链表末尾节点的指针区存放该插入节点的地址。
int TailInsertNode (LinkNode *pHeadNode, DataType tmpData) {
LinkNode *pFlow = NULL ;
LinkNode *pInsertNode = NULL ;
pInsertNode = malloc (sizeof (LinkNode));
if (pInsertNode == NULL ) {
perror("fail to malloc" );
return -1 ;
}
pInsertNode->data = tmpData;
pInsertNode->pNextnode = NULL ;
pFlow = pHeadNode->pNextnode;
while (pFlow->pNextnode != NULL ) {
pFlow = pFlow->pNextnode;
}
pFlow->pNextnode = pInsertNode;
return 0 ;
}
操作 生成链表 头插入 尾插入 共同点 定义一个指针,申请节点空间(插入必备) 数据区 不做处理 存放插入数值 存放插入数值 地址区 NULL 地址 首位有效节点的地址 NULL 地址 插入节点地址的存放 函数返回值 头节点指针区 原末尾节点指针区
四步插入法 :申请插入空间,寻找插入位置,存放插入内容,建立连接。
4. 节点的遍历(节点数据显示) 遍历方式 1
实现思路:判断指向当前节点的指针是否为 NULL。
while (pNowNode != NULL ) {
pNowNode = pNowNode->pNextnode;
}
遍历方式 2
实现思路:当前节点的指针区为 NULL,则其为末尾节点。
while (pNowNode->pNextnode != NULL ) {
pNowNode = pNowNode->pNextnode;
}
项目 遍历方式 1 遍历方式 2 目的 实现对每个有效节点数据区内容的遍历 遍历至末尾节点 实现方式 指向当前节点的指针是否为 NULL 当前节点的指针区是否存放 NULL 应用 节点数据的显示 尾插入法的实现
应用:节点数据的显示
实现思路:定义一个流动指针,从链表首个有效节点开始遍历,实现对各有效节点数据区内容的访问,并展示数据区具体内容。
void NodeDataShow (LinkNode *pHeadNode) {
LinkNode *pFlow = NULL ;
pFlow = pHeadNode->pNextnode;
while (pFlow != NULL ) {
printf ("%d " , pFlow->data);
pFlow = pFlow->pNextnode;
}
printf ("\n" );
return ;
}
5. 节点的删除 实现思路:定义两个指针,流动指针和前节点指针;流动指针用于遍历链表,查询删除节点;前节点指针用于建立删除节点的前后节点的连接关系。
流动指针采用遍历方式 1,从链表的首个有效节点开始,基于对有效节点数据区内容的匹配,查询到要删除的节点。
前节点指针实时指向流动指针指向节点的前一节点,流动指针到达删除节点时,前节点指针借助流动节点的指针区内容直接指向删除节点的后一节点,建立前后节点的连接关系。
通过流动指针释放删除节点的堆空间,同时借助前节点指针使流动指针指向删除节点的后节点,避免野指针。
int DeleteNode (LinkNode *pHeadNode, DataType rmData) {
int cnt = 0 ;
LinkNode *pPreNode = NULL ;
LinkNode *pNowNode = NULL ;
pPreNode = pHeadNode;
pNowNode = pHeadNode->pNextnode;
while (pNowNode != NULL ) {
if (pNowNode->data == rmData) {
pPreNode->pNextnode = pNowNode->pNextnode;
free (pNowNode);
pNowNode = pPreNode->pNextnode;
cnt++;
} else {
pPreNode = pPreNode->pNextnode;
pNowNode = pNowNode->pNextnode;
}
}
return cnt;
}
6. 节点的查询
7. 节点数据的更改
8. 链表的销毁 定义两个指针(遍历指针,释放指针),均从头节点开始。遍历指针只要非空,则后移,释放指针释放掉当前的,而后后移。
操作小结:创建、插入、移除、遍历展示、更改、查询、销毁。
复杂操作
1. 寻找中间节点 实现思路:快慢指针法。快指针走两格,慢指针走一个。快指针先走一格,判断是否为空指针,若为空,停止前进。
2. 找倒数第 k 个节点 实现思路:差额指针法。快指针先走 k 格,随后快慢指针一同前进,快指针到空指针处。
3. 倒置 实现思路:头插法实现倒置。定义两个指针(遍历指针/流动,节点插入指针/用于节点插入),使其均指向链表首位有效节点,断开头节点和链表的连接,进入遍历。
4. 冒泡排序 定义 2 个遍历(流动)指针,1 个终止指针,1 个暂存数据(用于数据交换)。先判断链表中有效数据的个数,有效数据个数为 0 个或 1 个时,直接退出,无需排序。进入排序,给遍历指针 1 号首位有效节点,给遍历指针 2 号第二位有效节点。
5. 选择排序 定义指针:遍历节点指针,记录最小值节点的指针,需要交换节点的指针。寻找最小值,比较,交换,后移。设定未排序部分的首位节点为可交换节点,并假设未排序部分的首位节点为最小值节点。遍历从第二位节点开始,若有遍历数据小于最小值节点的数据,则更新最小值节点(非数据交换)。完成一次遍历后,比较最小值节点的数据和未排序部分的首位节点的数据,若小于,则发生数据交换,以完成前小后大的排序。直到未排序部分的首位节点到达链表的最后一个节点。
6. 头节点未知,删除某位节点(了解) 只能删除中间节点,无法删除末尾节点。后一位节点的数据赋值给目标位节点,删除后一位节点。
7. 判断链表是否有环 判断是否有环,获得环长,获得环入口。
倍数遍历指针:单倍遍历指针,二倍遍历指针,两指针差值增长,每次增长一(自然增长)。指针重逢,差值即为环长。
环长 :相遇不一定在环入口。
双向链表
插入
申请空间 pNewNode。
插入节点的数据存放:存放数据到 pNewNode->data。
插入节点的指针存放:将 pNewNode->next 赋值为空白节点的 next,pNewNode->pre 赋值为空白节点地址。
将空白节点的 pNext 赋值为新节点。
后续有节点,需要将后面节点的 pre 指向新申请的节点。
int InsertHeadDouList (LinkNode *pHead, DataType TmpData) {
LinkNode *pInsertNode = NULL ;
pInsertNode = malloc (sizeof (LinkNode));
if (pInsertNode == NULL ) {
perror("fail to malloc" );
return -1 ;
}
pInsertNode->data = TmpData;
pInsertNode->pPre = pHead;
pInsertNode->pNext = pHead->pNext;
pHead->pNext = pInsertNode;
if (pInsertNode->pNext != NULL ) {
pInsertNode->pNext->pPre = pInsertNode;
}
return 0 ;
}
删除
查找删除节点。
让删除节点的前面节点的 next 指向删除节点的 next。
如果有后面节点,让删除节点的后面节点的 pre 指向删除节点的 pre。
定义 pFree 指向待释放节点。
释放。
存放数据的类型:遍历和单向无差(可反向遍历)。查找、修改也是(遍历找元素,修改)。销毁也一样。
双向循环链表
区别 链表指针区不会存放空指针。遍历终止条件判断更改为是否指向头节点。无需判断后节点为空的情况。寻找末尾节点无需一一顺向遍历,直接取头节点的前节点即可。
LinkNode *CreateEmptyCirList (void ) {
LinkNode *pHead = NULL ;
pHead = malloc (sizeof (LinkNode));
if (pHead == NULL ) {
perror("fail to malloc" );
return NULL ;
}
pHead->pPre = pHead;
pHead->pNext = pHead;
return pHead;
}
int InsertHeadCirList (LinkNode *pHead, DataType TmpData) {
LinkNode *pInsertNode = NULL ;
pInsertNode = malloc (sizeof (LinkNode));
if (pInsertNode == NULL ) {
perror("fail to malloc" );
return -1 ;
}
pInsertNode->data = TmpData;
pInsertNode->pPre = pHead;
pInsertNode->pNext = pHead->pNext;
pInsertNode->pPre->pNext = pInsertNode;
pInsertNode->pNext->pPre = pInsertNode;
return 0 ;
}
int InsertTailCirList (LinkNode *pHead, DataType TmpData) {
LinkNode *pInsertNode = NULL ;
pInsertNode = malloc (sizeof (LinkNode));
if (pInsertNode == NULL ) {
perror("fail to malloc" );
return -1 ;
}
pInsertNode->data = TmpData;
pInsertNode->pNext = pHead;
pInsertNode->pPre = pHead->pPre;
pInsertNode->pPre->pNext = pInsertNode;
pInsertNode->pNext->pPre = pInsertNode;
return 0 ;
}
插入必须通过头节点来建立插入节点和链表的连接关系。
int ShowCirList (LinkNode *pHead) {
LinkNode *pFlow = NULL ;
pFlow = pHead->pNext;
while (pFlow != pHead) {
printf ("%d " , pFlow->data);
pFlow = pFlow->pNext;
}
printf ("\n" );
return 0 ;
}
链表拓展 内核链表
目的:设置一种复用性高的链表(数据类型多样,不仅仅是整型)。
数据类型由函数调用者确定。建立结构体,存放链表的节点和数据内容。链表的节点只存放前后指针,只有节点指针,没有数据类型。数据访问的实现:节点的首地址和整个结构体的首地址一样,强制类型转换将节点指针改为结构体指针,从而实现对数据的访问。
面试问题 对比项 数组 链表 存储空间 连续 不连续 元素个数 必有限 没有上限 插入、删除元素的效率 比较低 比较高 访问元素 比较方便 不方便
数组的空间是连续的,链表的空间可以不连续。数组元素必有限,链表元素是没有上限的。数组插入和删除效率比较低,链表的插入和删除效率比较高。数组访问元素比较方便,链表访问元素不方便。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online