一.前言
数据结构思维导图如下,灰色标记的是之前讲过的,本文将带你走近单链表(红色标记部分),希望大家有所收获🌹🌹

二.链表的定义和概念
在讲单链表之前,我们先学习一下链表
2.1 链表的定义
链表是一种物理存储单元上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,即
- 逻辑结构:线性
- 物理结构:非线性
2.2 节点的构成要素
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
每个结点包括两个部分:一个是存储数据元素的数据域 ,另一个是存储下一个结点地址的指针域。即数据+指向下一个结点的指针,节点常用结构体实现。
c
struct ListNode
{
LTDataType data;
struct ListNode* next;
};
2.3 与数组结构的对比差异
数组 | 链表 | |
---|---|---|
存储方式 | 连续存储 需要预先分配固定大小的内存空间 | 非连续存储,通过指针连接 动态分配内存,不需要预先确定大小 |
内存使用 | 空间利用率高 (因为所有元素都紧密排列) 可能存在未使用的空间(e.g.数组大小固定但实际使用元素少) | 空间利用率相对较低 每个节点需要额外的内存来存储指针 |
插入和删除操作 | 可能需要移动大量元素以保持连续性 时间复杂度为O(n),因为可能需要移动插入点之后的所有元素 | 通常只需要改变指针,不需要移动元素 时间复杂度为O(1) (已知插入或删除位置的节点) |
随机访问 | 支持高效的随机访问,可通过索引快速访问任意元素 访问时间复杂度为O(1) | 不支持高效的随机访问,必须从头节点开始遍历链表 访问时间复杂度为O(n)。 |
空间局部性 | 有很好的空间局部性,因为元素连续存储,适合缓存优化 | 空间局部性差,因为元素分散存储,可能导致缓存未命中 |
实现复杂度 | 实现简单,大多数编程语言内置支持 | 实现相对复杂,需要手动管理节点和指针 |
适用场景 | 适合需要频繁随机访问的场景 适合元素数量已知且变化不大的场景 | 适合插入和删除操作频繁的场景 适合元素数量频繁变化的场景 |
三.链表的分类
链表的结构非常多样,以下情况组合起来就有8种(2*2*2)链表结构
链表说明:
虽然有这么多的链表结构,但是我们实际中最常用的还是两种结构:
- 单链表:不带头单向不循环链表
- 双向链表:带头双向循环链表
四.单链表
前期准备
创建三个文件:
- SList.h: 存放各种头文件和函数声明
- SLIst.c : 各种函数的实现
- test.c: 测试和执行代码 最好写完一部分测试一部分 防止后期调试一堆bug
实现
首先在头文件中写上
c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
接下来我们一步步实现单链表各种操作
1.定义链表结构
c
typedef int SLTDataType;//方便之后一键替换类型
typedef struct SListNode
{
SLTDataType data; //结点数据
struct SListNode* next; //指针保存下⼀个结点的地址
}SLTNode;
2.申请新节点和打印函数
因为后面会多次用到申请新节点和打印函数,所以我们把它们都各自封装成函数方便调用
c
//申请新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = NULL;
return node;
}
//打印函数
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
3.查找数据
c
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead);
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到
return NULL;
}
4.插入操作
包括尾插、头插、在指定位置之前插入数据、在指定位置之后插入数据
(1)尾插
c
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//pphead --> &plist
// *pphead --> plist
//申请新节点
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾结点
SLTNode* pcur = *pphead;
while (pcur->next)
{
pcur = pcur->next;
}
//pcur newnode
pcur->next = newnode;
}
}
(2)头插
c
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//newnode *pphead
newnode->next = *pphead;
*pphead = newnode;
}
(3)在指定位置之前插入数据
c
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* newnode = SLTBuyNode(x);
//找prev :pos的前一个结点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev newnode --> pos
newnode->next = pos;
prev->next = newnode;
}
}
(4)在指定位置之后插入数据
c
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//pos newnode --> pos->next
newnode->next = pos->next;
pos->next = newnode;
}
测试如下
5.删除操作
包括尾删、头删、删除指定节点、删除指定位置之后的结点、删除指定位置之前的结点(该操作是给你们留的作业😜 学完这篇自己独立实现一下 答案可以让我发给你或者其实调试通过了应该没什么问题跟前面的实现逻辑一样)
(1)尾删
c
void SLTPopBack(SLTNode** pphead)
{
//链表为空:不可以删除
assert(pphead && *pphead);
//处理只有一个结点的情况:要删除的就是头结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//找 prev ptail
SLTNode* ptail = *pphead;
SLTNode* prev = NULL;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
测试如下
(2)头删
c
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
//*pphead --> next
free(*pphead);
*pphead = next;
}
(3)删除指定节点
c
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//头删
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
(4)删除指定位置之后的结点
c
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
//pos pos->next pos->next->next
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
6.销毁链表
c
void SListDestroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}

五.总结
单链表的详细实现代码已经上传到我的资源了,大家点进主页或者在本文最上方可以下载查看,自己去写一下,边写边调试会更容易理解和掌握。
接下来会逐步介绍上述思维导图数据结构剩下的部分,创作不易,希望大家多多支持,有什么想法欢迎讨论🌹🌹