线性表之单链表(详解)
🍕博客主页:️自信不孤单
🍬文章专栏:数据结构与算法
🍚代码仓库:破浪晓梦
🍭欢迎关注:欢迎大家点赞收藏+关注
文章目录
- 🍥前言
- 🍉链表
- 1. 链表的概念及结构
- 2. 链表的分类
- 3. 单链表的实现
- 3.1 动态申请一个节点
- 3.2 打印单链表
- 3.3 单链表尾插
- 3.4 单链表尾删
- 3.5 单链表头插
- 3.6 单链表头删
- 3.7 单链表查找
- 3.8 在指定位置后插入数据
- 3.9 在指定位置前插入数据
- 3.10 删除指定位置之后的数据
- 3.11 删除指定位置的数据
- 3.12 单链表的销毁
- 4. 接口测试
🍥前言
在前一文章我们已经学习了顺序表,但是我们发现顺序表还有一些小缺点满足不了我们的需求,例如:
- 中间/头部的插入删除,时间复杂度为O(N)。
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。
基于这些问题,我们来学习一种新的数据结构——链表,而链表就可以完美解决以上问题了。
在这篇文章中我们来重点学习一下单链表的实现。
🍉链表
1. 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
理解
链表是由一系列结点组成的,结点可动态的生成。每个节点是一个结构体,而且每一个节点在堆上的开辟是随机的,我们可以通过结构体指针来维护每一个节点,将每一个节点链接起来,这样就形成了一条链。
链表中的节点是这样的:
- DataType 表示要存放的某类型的数据。
- *next 表示该结构体类型的指针,一般将此指针赋值为下一个结点的地址,这样就可以通过这个节点的指针找到下一个节点了。
链表的结构:
2. 链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 单向或者双向
- 带头或者不带头
- 循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是用代码实现以后会发现结构会带来很多优势,实现反而简单了。
3. 单链表的实现
首先来创建两个文件来实现单链表:
- SList.h(节点的声明、接口函数声明、头文件的包含)
- SList.c(单链表接口函数的实现)
接着创建 test.c 文件来测试各个接口
如图:
SList.h 文件内容如下:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SListNode;
// 动态申请一个节点
SListNode* BuySListNode(SLTDataType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x);
// 在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDataType x);
// 在pos位置之前插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDataType x);
// 删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);
接下来,我们在 SList.c 文件中实现各个接口函数。
3.1 动态申请一个节点
在堆上申请一个节点结构体大小的空间,并用该节点存放数据 x,节点的 next 指针指向 NULL,返回节点的地址。
SListNode* BuySListNode(SLTDataType x)
{
SListNode* ret = (SListNode*)malloc(sizeof(SListNode));
if (NULL == ret)
{
perror("malloc fail");
return NULL;
}
ret->data = x;
ret->next = NULL;
return ret;
}
3.2 打印单链表
注意:这里不需要的 plist 进行断言。plist 为空,则打印 NULL。
void SListPrint(SListNode* plist)
{
while (plist)
{
printf("%d->", plist->data);
plist = plist->next;
}
printf("NULL\n");
}
3.3 单链表尾插
尾插分为两种情况:
- 当链表为空时,头指针 plist 指向 NULL。此时要改变 plist 的指向,让 plist 指向新开辟好的结点,就需要用二级指针来改变一级指针的值(如果传参传的是 plist,并用一级指针作为形参,形参的改变不会影响实参,那么 plist 就不会被改变)。
- 当链表非空时,只需通过循环找到尾节点,并将为节点的 next 指针赋值为新开辟好节点的地址。
void SListPushBack(SListNode** pplist, SLTDataType x)
{
assert(pplist);
if (*pplist)
{
SListNode* cur = *pplist;
while (cur->next)
{
cur = cur->next;
}
cur->next = BuySListNode(x);
}
else
{
*pplist = BuySListNode(x);
}
}
3.4 单链表尾删
分两种情况讨论即可。
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* cur = *pplist;
if (cur->next)
{
while (cur->next->next)
{
cur = cur->next;
}
free(cur->next);
cur->next = NULL;
}
else
{
free(cur);
cur = NULL;
*pplist = NULL;
}
}
3.5 单链表头插
void SListPushFront(SListNode** pplist, SLTDataType x)
{
assert(pplist);
SListNode* tmp = *pplist;
*pplist = BuySListNode(x);
(*pplist)->next = tmp;
}
3.6 单链表头删
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* del = *pplist;
*pplist = (*pplist)->next;
free(del);
del = NULL;
}
3.7 单链表查找
返回所找到节点的指针,没找到则返回 NULL。
注:查找函数可以配合指定位置操作函数来使用。
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
SListNode* cur = plist;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
3.8 在指定位置后插入数据
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* tmp = pos->next;
pos->next = BuySListNode(x);
pos->next->next = tmp;
}
3.9 在指定位置前插入数据
注意分情况讨论,判断 pos 位置是否为头指针的位置。
void SListInsert(SListNode** pplist, SListNode* pos, SLTDataType x)
{
assert(pos);
assert(pplist);
if (*pplist == pos)
{
SListPushFront(pplist, x);
return;
}
SListNode* cur = *pplist;
while (cur)
{
if (cur->next == pos)
{
SListNode* tmp = cur->next;
cur->next = BuySListNode(x);
cur->next->next = tmp;
return;
}
cur = cur->next;
}
}
3.10 删除指定位置之后的数据
void SListEraseAfter(SListNode* pos)
{
assert(pos);
assert(pos->next);
SListNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
3.11 删除指定位置的数据
注意分情况讨论,判断 pos 位置是否为头指针的位置。
void SListErase(SListNode** pplist, SListNode* pos)
{
assert(pos);
assert(pplist);
if (*pplist == pos)
{
free(*pplist);
*pplist = NULL;
return;
}
SListNode* cur = *pplist;
while (cur)
{
if (cur->next == pos)
{
SListNode* del = pos;
cur->next = del->next;
free(del);
return;
}
cur = cur->next;
}
}
3.12 单链表的销毁
不要忘记把 plist 置空。
void SListDestroy(SListNode** pplist)
{
assert(pplist);
SListNode* del = *pplist;
while (del)
{
SListNode* cur = del->next;
free(del);
del = cur;
}
*pplist = NULL;
}
注:在每个接口函数中一定要合理地使用assert函数断言防止对空指针的引用。
4. 接口测试
test.c 文件内容如下:
#include "SList.h"
void Test()
{
SListNode* plist = NULL;
//头插
SListPushFront(&plist, 3);
SListPrint(plist);
SListPushFront(&plist, 1);
SListPrint(plist);
//尾插
SListPushBack(&plist, 5);
SListPrint(plist);
//指定位置后插
SListNode* insert = SListFind(plist, 3);
SListInsertAfter(insert, 4);
SListPrint(plist);
//指定位置前插
SListInsert(&plist, insert, 2);
SListPrint(plist);
//头删
SListPopFront(&plist);
SListPrint(plist);
SListPopFront(&plist);
SListPrint(plist);
//尾删
SListPopBack(&plist);
SListPrint(plist);
//指定位置后删
SListNode* del = SListFind(plist, 3);
SListEraseAfter(del);
SListPrint(plist);
//指定位置删除
SListErase(&plist, del);
SListPrint(plist);
SListDestroy(&plist);
}
int main()
{
Test();
return 0;
}
运行结果: