目录
- 一.单链表的引入
- 二.单链表头文件展示
- 三.单链表逐个函数讲解
-
- 1.动态申请一个节点
- 2.单链表打印
- 3.单链表的尾插
- 4.单链表的头插
- 5.单链表的头删
- 6.单链表的尾删
- 7.单链表数据的查找
- 8.单链表任意位置插入
- 9.单链表任意位置删除
- 四.单链表总结
一.单链表的引入
因为在顺序表存储的过程中,当空间不够时,会集中进行空间的扩容。在后期使用期间,如果空间需求很小就会造成空间的浪费。但是顺序表也有自己的优点,比如:可以快速查找数据。一下是两种存储结构的对比:
| 对比维度 | 顺序表 | 单链表 |
|---|---|---|
| 内存分配 | 连续内存空间 | 离散内存空间(堆区动态申请节点) |
| 访问方式 | 随机访问,通过下标O(1)直接定位 | 顺序访问,O(n)遍历指针 |
| 插入删除 | 中间元素O(n),两头元素O(1)。需要移动后续元素 | 中间插入O(1),需要O(n)查找前驱指针。 |
| 空间效率 | 无额外开销,但是可能存在冗余空间 | 按需申请节点,动态扩容。 |
| 底层依赖 | 依赖数组的下标映射机制 | 依赖指针的地址关联机制 |
| 适用场景 | 频繁数据访问,数据量稳定 | 频繁插入删除,数据量频繁动态改变 |
二.单链表头文件展示
c
// 1、无头+单向+非循环链表增删查改实现
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
上述代码的具体解释:首先利用关键字typedef将int更改为SLTDateType,方便后续处理更多数据类型的数据。接下来创建节点结构体,结构体成员变量分别是数据本身和下一个节点的指针。后面的代码全都是相关函数的声明,具体函数实现将在下面一一讲解。
三.单链表逐个函数讲解
1.动态申请一个节点
c
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x)
{
SListNode * newNode=(SLTDateNode *)malloc(sizeof (SListNode));
if (newNode == NULL)
{
perror("malloc fail");
exit(-1);
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
上述代码的具体解释:利用函数malloc函数向操作系统申请新的空间,然后将其强制类型转换为结构体指针类型并且赋值给newNode。如果newNode为空指针,则说明申请失败,打印失败信息,返回-1表示非正常返回。申请成功则将结构体成员初始化即可。
2.单链表打印
c
void SListPrint(SLTNode *plist)
{
SLTNode *cur =plist;
while(cur!= NULL)
{
printf("%d",cur->data);
cur=cur->next;
}
}
上述代码的具体解释:首先创建一个指针变量指向单链表头节点的位置,然后遍历单链表的所有元素,打印所有节点的数据,
3.单链表的尾插
c
void SListPushBack(SLTNode ** pphead,SLDataType x)
{
SLTNode * newnode=(SLTNode *)malloc(sizeof (SLTNode));
newnode->data=x;
newnode->next=NULL;
if(* pphead ==NULL)
{
*pphead=newnode;
}
else
{
SLTNode *tail =*pphead;
while(tail->next!=NULL)
{
tail =tail->next;
}
tail->next=newnode;
}
}
上述代码的具体解释:因为尾插会改变头节点的数据,所以函数的形式参数需要用到二级指针。接下来申请空间用于存放尾插的数据,如果单链表的内容为空,则直接将申请好的空间赋值给头指针。如果单链表内容不是空,则首先遍历链表到最后一个节点处,然后将申请来的节点地址赋值给最后一个节点的next指针处。
4.单链表的头插
c
void SListPushFront(SLTNode **pphead,SLTDataType x)
{
SLTNode *newnode=BuySListNode(x);
newnode->next=*pphead;
*pphead=newnode;
}
上述代码的具体解释:首先向操作空间申请一个空间用于存放头插的数据,将该数据的next指针指向头节点的地址,更新头节点的位置到新申请的位置处。
5.单链表的头删
c
void SListPopFront(SLTNode **pphead)
{
SLTNode*next=(*pphead)->next;
free(*pphead);
*pphead=next;
}
上述代码的具体解释:首先将头节点存储的next指针复制给next。(提前保存,防止释放头节点之后的数据丢失)最后将next指针赋值给头节点就完成了单链表的头删。
6.单链表的尾删
c
void SListPopBack(SLTNode ** pphead)
{
if (*pphead ==NULL)
{
return;
}
else if((*pphead)->next==NULL)
{
free(*pphead);
*pphead=NULL;
}
else
{
SLTNode *prev=NULL;
SLTNode *tail=*pphead;
while(tail->next!=NULL)
{
prev=tail;
tail=tail->next;
}
free(tail);
prev->next=NULL;
}
}
上述代码的具体解释:当单链表为空链表时,直接返回不用继续进行删除数据。当单链表为一个节点时,直接释放掉这个节点的空间就可以完成尾删。当单链表为一个以上的节点时,需要先遍历到倒数第二个节点的位置,然后释放最后一个节点的空间。
所以问题简化为如何遍历到倒数第二个节点的位置。这里需要用到两个指针,首先将两个指针同时指向头部位置,然后只要tail的next指针不为NULL,就将另外一个指针prev指向该tail的位置。直到tail的next指针为NULL时,则prev指向的位置就是倒数第二个节点。随后将倒数第一个节点进行空间的释放,将倒数第二个节点的next指针置为NULL。变成新的最后一个节点。
7.单链表数据的查找
c
SLTNode * SListFind(SLTNode *phead,SLTDataType x)
{
SListNode *cur=phead;
while(cur!=NULL)
{
if(cur->data==x)
{
return cur;
}
cur =cur->next;
}
return NULL;
}
上述代码的具体解释:要找到数据为x的单链表位置,直接用while循环开始遍历,找到则返回当前指针。没有找到则返回NULL。
8.单链表任意位置插入
c
void SListInsert(SLTNode**pphead,SLTNode*pos,SLTDaType x)
{
if(pos ==*pphead)
{
SListPushFront(pphead,x);
}
else
{
SLTNode *newnode= BuySListNode(x);
SLTNode *prev= *pphead;
while(prev->next!=pos)
{
prev=prev->next;
}
prev->next=newnode;
newnode->next=pos;
}
}
上述代码的具体解释:当任意位置为头部时,直接调用头插函数即可。当任意位置不为头时,需要先申请空间用于存储需要插入的数据。接下来遍历单链表到要插入的位置前,进行数据的插入。这里分情况谈论是因为第二种情况在头插时会出现问题。(prev的next指针没有查看是否等于pos该位置)。
9.单链表任意位置删除
c
void SListErase(SLTNode **pphead,SLTNode*pos)
{
if(pos==*pphead)
{
SListPopFront(pphead);
}
else
{
SLTNode*prev=*pphead;
while(prev->next!=pos)
{
prev=prev->next;
}
prev->next=pos->next;
free(pos);
}
}
上述代码的具体解释:与上述任意位置插入相同,任意位置删除也需要分两种情况谈论。当任意位置为头部时,调用头删函数即可。当不是头删时,则需要用相同的方法遍历到需要删除位置的前一个,然后将删除位置的前后两个节点对接,最后释放需要删除数据的空间即可。
四.单链表总结
单链表相比于顺序表有些许的优点,在本章内容开头详细说明了。但是单链表也有一些缺点待解决。接下来要讲的双向链表就会解决一下单链表的问题。
- 反向访问困难,无法直接通过节点获取前驱,需要从单链表头部遍历,时间复杂度O(n)。
- 删除指定节点效率低,如果仅知道节点指针。需要先遍历找前驱。
- 遍历灵活性差,仅支持从表头到表尾的单向遍历,无法双向切换。