【数据结构】——3.1单链表
目录
一、链表的概念
1. 链表的概念及结构
链表(Linked List):物理结构上非连续、非顺序,逻辑结构上顺序存储的一种存储结构。链表中的逻辑顺序是通过指针链接的
- 链表在逻辑上是连续的,物理上是非连续的
- 结点一般是在堆上申请的,如果在栈上,函数调用结束后就会释放,所有操作都在一个函数内才能使用,显然这是不行的
2. 链表的分类
实际中的链表非常多样,以下情况组合起来就有8种链表结构
- 单向或双向
- 带头或不带头
- 循环或非循环
虽然链表结构很多,但是我们实际种最常用的就是两种:无头单向非循环链表 和 带头双向循环链表
二、单链表的概念和结构
1. 单链表的概念
无头单向非循环链表:结构简单的链表,不会用来单独存储数据,实际中是作为其他数据结构的子结构,如哈希桶、图的邻接表等
2. 单链表结构
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data; //数据
struct SListNode* next; //指向下一个结点的指针
} SListNode;
三、单链表的实现
1. 动态申请结点
- 必须使用动态申请空间,否则调用结束后栈空间被释放
- 空间申请完成后赋值数值,初始化指针NULL
SListNode* BuySListNode(SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
2. 遍历打印单链表
遍历单链表,依次打印结点的数据
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur != NULL)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
3. 插入结点
3.1 头插
- 必须先将新结点next指向第一个结点,再将头指针指向新节点
- 若是先将头指针指向新结点,则头指针指向的第一个结点的地址就会丢失,造成内存泄漏
void SListPushFront(SListNode** pplist, SLTDataType x)
{
assert(pplist != NULL);
SListNode* newnode = BuySListNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
3.2 尾插
- 结构体为空时,改变头指针的指向
- 结构体不为空时,改变结构体中next的指向
- 尾插之前要遍历链表,找尾
//尾插
void SListPushBack(SListNode** pplist, SLTDataType x)
{
assert(pplist != NULL);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL) //结构体为空
{
*pplist = newnode;
}
else //结构体不为空
{
SListNode* cur = *pplist;
while (cur->next != NULL)
{
cur = cur->next;
}
cur->next = newnode;
}
}
3.3 插入
- 链表的插入默认是前插,要遍历链表找到
pos
结点的前一个结点,再进行插入 - 插入函数的后插很容易实现,函数库里有后插函数,在此不做实现
pos
结点要检查是否为空- 若
pos
为头结点时,它的next为NULL,循环中再次访问NULL的next时会报错,所以要单独处理,直接调用头插函数即可 - 若是
pos
不在链表内则使用断言中断程序
void SListInsert(SListNode** pplist, SListNode* pos, SLTDataType x)
{
assert(pplist != NULL && pos != NULL);
if (*pplist == pos) //pos为头结点时,作为头插,直接调用函数
{
SListPushFront(pplist, x);
}
else
{
SListNode* cur = *pplist;
while (cur->next != pos)
{
cur = cur->next;
assert(cur != NULL); //pos不在链表中
}
SListNode* newnode = BuySListNode(x);
newnode->next = pos;
cur->next = newnode;
}
}
4. 删除结点
4.1 头删
- 删除前检查链表是否有数据,链表没有数据时,可以使用断言处理(在这里不做处理)
- 释放第一个结点前要先用指针记录第二个结点,否则第一个结点后面的结点都失去指针指引,造成内存泄漏
void SListPopFront(SListNode** pplist)
{
assert(pplist != NULL);
if (*pplist == NULL)
{
return;
}
SListNode* cur = *pplist;
*pplist = (*pplist)->next;
free(cur);
cur = NULL;
}
4.2 尾删
- 删除前检查链表是否有数据,链表没有数据时,可以使用断言处理(在这里不做处理)
- 删除时要找到删除结点的上一个结点,将上一个结点的next置为NULL
- 当只有一个结点时,第一个结点的next为NULL,遍历查找上一个结点时,要对当前结点的next的next进行访问。,访问其next时会报错,所以要检查是否只有一个结点,一个结点时只删除一个结点,将头指针置为NULL
void SListPopBack(SListNode** pplist)
{
assert(pplist != NULL);
if (*pplist == NULL)
{
return;
}
if ((*pplist)->next == NULL) //只有一个结点
{
free(*pplist);
*pplist = NULL;
}
else //有多个结点
{
SListNode* cur = *pplist;
while (cur->next->next != NULL)
{
cur = cur->next;
}
free(cur->next);
cur->next = NULL;
}
}
4.3 删除
- 删除第一个结点时,直接释放,头指针置为NULL
- 删除其他结点时,遍历找到上一个结点,再删除
void SListErase(SListNode** pplist, SListNode* pos)
{
assert(pplist != NULL && pos != NULL);
if (*pplist == NULL)
{
return;
}
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* cur = *pplist;
while (cur->next != pos)
{
cur = cur->next;
assert(cur != NULL);
}
cur->next = pos->next;
free(pos);
pos = NULL;
}
}
5. 查找数据
- 遍历链表,比较数值,返回值为指定数据的结构体指针
- 调用者可以通过调用该函数获取指定数据的结点指针来修改数据,达到修改数据的目的
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
SListNode* cur = plist;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
6. 销毁单链表
销毁时从前往后释放,释放之前要先用临时变量保存下一个结点的地址,防止内存泄漏(这里使用头指针保存下一个结点地址)
void SListDetroy(SListNode** pplist)
{
assert(pplist != NULL);
SListNode* cur = *pplist;
while (cur != NULL)
{
*pplist = cur->next;
free(cur);
cur = *pplist;
}
}
四、单链表的相关问题
1. 单链表实现中的问题
1.1 初始化问题
单链表不需要初始化,单链表为空时头指针为NULL,不需要其他操作
1.2 二级指针问题
- 为什么要使用二级指针作为参数传递单链表
- 单链表修改头指针时要传入二级指针,因为头指针需要传址调用,一级指针是头指针的拷贝,修改时不改变调用者的一级指针
- 在进行插入(头插、尾插、插入) 、 删除(头删、尾删、删除) 和 销毁时需要传入二级指针,因为有链表为空的情况,需要对头指针进行修改
- 在进行打印、查询操作时不用传二级指针,不对头指针内容进行修改
- 不使用二级指针的解决方案(两种)
- 使用带头结点的链表,链表没有元素时头指针指向头结点,不需要对头指针修改
- 调用函数时以返回值的形式返回头指针,让头指针在调用时赋值(非常麻烦)
1.3 断言问题
- 参数为一级指针不需要断言,单链表为空时头指针为NULL,是合法的情况
- 参数为二级指针需要断言,二级指针存储着头指针的地址,若是为NULL,则头指针不存在,是非法情况
五、完整代码
代码保存在gitee中:点击完整代码参考