上篇文章已经讲到顺序表的缺陷,因此我们引出了链表结构,而链表中最简单的一种结构便是单链表,这篇文章会详细介绍单链表相关知识,带你从浅入深掌握单链表
单链表结构
不同于数组,单链表并不是一块连续的存储空间,它是由一个个节点构成的,而每个节点的类型是结构体,其中包含了两个成员变量,第一个被称为数据域,就是用来存放数据的,第二部分被称为指针域,存放的是一个指针变量,指针变量存放的是下一个节点的地址
物理结构(内存中实际存储)如下:
···每个方框代表了一个节点,每个节点里面存有数据和下一个节点的地址,因此只要知道了第一个节点的地址,就能依次找到其余节点了。
···最后一个节点里面的指针是NULL,因为没有下一个节点了,便置成空
···最后一个节点这些节点的地址都是随机的(malloc开辟的),因此不存在下标访问这一特点。
逻辑结构(假想出来的箭头)如下:
指针存储了哪块空间的地址就会指向哪块空间,因此每个next指向了下一个节点
单链表增删查改等操作
和通讯录,顺序表的实现相似,我们一般都分三个文件来写
SList.h 头文件的包含,结构体的定义,函数的声明,类型重定义等等
SList.c 增删查改等函数的实现
test.c 测试项目功能
先给出完整代码
Slist.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//单链表打印
void SLTPrint(SLTNode* phead);
//单链表尾插
void SLPushBack(SLTNode** pphead, SLTDataType x);
//单链表头插
void SLPushFront(SLTNode** pphead, SLTDataType x);
//单链表尾删
void SLPopBack(SLTNode** pphead);
//单链表查找
SLTNode* SLFind(SLTNode* phead, SLTDataType x);
//插入
void SLInsertFront(SLTNode* phead, SLTNode* pos, SLTDataType x);
void SLInsertAfter(SLTNode* pos, SLTDataType x);
//删除
void SLErase(SLTNode** pphead, SLTNode* pos);
void SLEraseAfter(SLTNode* pos);
//销毁
void SLDestroy(SLTNode** pphead);
Slist.c
#include "SList.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;//迭代
}
printf("NULL\n");
}
//创建新节点
SLTNode* BuyLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//头插
void SLPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newNode = BuyLTNode(x);
newNode->next = *pphead;
*pphead = newNode;
}
//
//尾插
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuyLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//尾删
void SLPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* tail = *pphead;
SLTNode* prev = NULL;
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
while (tail->next)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
void SLPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SLInsertFront(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newNode = BuyLTNode(x);
prev->next = newNode;
newNode->next = pos;
}
}
//pos之后插入
//void SLInsertAfter(SLTNode* pos, SLTDataType x)
//{
// SLTNode* newNode = BuyLTNode(x);
// SLTNode* next = pos->next;
// pos->next = newNode;
// newNode->next = next;
//}
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
SLTNode* newNode = BuyLTNode(x);
newNode->next = pos->next;
pos->next = newNode;
}
void SLErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead = pos)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
void SLEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
void SLDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
test.c
#include"SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
}
void TestSList2()
{
SLTNode* plist = NULL;
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLTPrint(plist);
}
void TestSList3()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLTPrint(plist);
}
void TestSList4()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLTPrint(plist);
printf("\n");
SLPopBack(&plist);
SLPopBack(&plist);
SLPopBack(&plist);
SLTPrint(plist);
}
void TestSList5()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
printf("\n");
SLPopFront(&plist);
SLPopFront(&plist);
SLPopFront(&plist);
SLTPrint(plist);
}
void TestSList6()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
printf("\n");
SLTNode* pos = SLFind(plist,3);
pos->data = 10;
SLTPrint(plist);
}
void TestSList7()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
printf("\n");
SLTNode* pos = SLFind(plist, 3);
SLInsertFront(&plist, pos, 20);
SLTPrint(plist);
}
void TestSList8()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
printf("\n");
SLTNode* pos = SLFind(plist, 1);
SLInsertFront(&plist, pos, 20);
SLTPrint(plist);
}
void TestSList9()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
printf("\n");
SLTNode* pos = SLFind(plist, 2);
SLInsertAfter(pos, 20);
SLTPrint(plist);
}
void TestSList10()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
printf("\n");
SLTNode* pos = SLFind(plist, 1);
SLErase(&plist, pos);
SLTPrint(plist);
}
void TestSList11()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLTPrint(plist);
printf("\n");
SLTNode* pos = SLFind(plist, 2);
SLEraseAfter(pos);
SLTPrint(plist);
}
int main()
{
//TestSList1();
//TestSList2();
//TestSList3();
//TestSList4();
//TestSList5();
//TestSList6();
//TestSList7();
//TestSList8();
//TestSList9();
//TestSList10();
//TestSList11();
}
------------------------------------------------------------------------------------------------------------------------
下文将对每一部分进行详细分析:
①结构体定义
第一行对int进行了类型重定义,我们目前针对的是整数,但是节点里面存储的数据不一定是整数,因此为了方便后续直接修改,进行了类型重定义
②打印
函数形参接受了外部传入的指向头节点的指针phead,在链表中我们一般不要改变头节点(改了之后就找不到头了),因此创建了一个新的变量cur,打印的关键便是遍历整个链表,cur = cur->next这条迭代语句,是找到每个节点的关键
①第一次进入循环后,打印出了1这个数字,cur的值被修改成0x11223344,于是cur指向了第二个节点,依次往后迭代
②循环结束的标志是cur == NULL,因为打印完最后一个节点后,再执行cur = cur->next,cur就会被置成空指针,因此循环继续的条件就是 while(cur != NULL) 或者直接写 while(cur)
③如果循环条件写成while(cur->next)便不能打印出最后一个节点的数据
③尾插
下面就涉及到了对链表的真正修改的操作,细心的小伙伴肯定发现了单链表和增删查改相关的函数形参都用的是二级指针,这是为什么呢?比如说尾插函数中,开始传进来的plist为空,插入了一个节点之后头节点需要更新为新的节点newnode,因此需要在函数内部改变外部的一级指针plist,因此需要使用二级指针;头插,尾删等也是类似的道理
由于尾插,头插等函数都需要创建新节点,因此我们把创建新节点封装成一个函数
创建新节点
创建节点的这个函数内部就直接把要插入的数据x放到了新节点的数据域中,把指针域先赋值成NULL指针,同时返回新节点的地址newnode
尾插
尾插的关键是找到尾节点,并让尾节点的next指向newnode
但是这种写法是有问题的,就是当链表为空的时候,,plist为空,*pphead为空,即tail为空,而tail->next变发生了空指针解引用问题,便会报错,因此应该分情况讨论链表为空的情况
正确代码
当链表为空的时候,直接让*pphead(也就是plist变成newNode即可),newNode就成为了新的头节点
这时在主函数中调用TestSList1函数尾插并打印,结果如下
总结:涉及到对单链表的改变的时候经常需要考虑三种情况,链表为空,链表只有一个节点,链表有多个节点,有时这三种情况可以用同一份代码一样,但经常需要单独考虑链表为空或者一个节点的情况,总之要养成这样思考的习惯,代码写出bug的可能才会降低
④头插
头插的逻辑很简单,把新的头节点和原来的头节点链接起来,然后让新创建的节点作为头节点即可
要注意的是,后面两句代码不能反过来写,因此如果先把旧的头节点赋值成新结点后,就找不到旧的头节点了,就无法进行链接了(当然先创建一个临时变量保存旧的头节点也可以,不过没必要
根据前面所说,应该再判断一下链表为空和链表只有一个节点的情况,经过判断发现,这段代码依旧适用,因此头插就这么一点代码
链表为空头插测试
链表不为空头插测试
⑤尾删
思路:
1.遍历找尾
2.释放尾节点
3.尾节点的前一个节点的next置空
关键点:找到尾节点的前一个节点
法一: 再定义一个指针变量prev,始终保存tail指向节点的前一个节点的地址
每次tail往后走的时候,先赋值给prev,再往后走
法二:
分析如下:
当tail走到目前这个位置的时候,tail->next->next为空,跳出循环,这时tail指向的是尾节点的前一个节点,然后释放最后一个节点即free(tail->next),将tail指向的next置空
不过目前两种写法都有问题,考虑一下链表为空和链表只有一个节点的情况!!!
(1)链表为空时没有节点,因此不能进行尾删,可以温柔 if 检查,也可以暴力 assert 断言
暴力检查:assert(*pphead);
温柔检查:if (*pphead == NULL)
return;
(2)链表只有一个节点时
写法一的问题:
(*pphead)->next == NULL,也就是tail->next为空,while没有进去,prev 仍为空指针,因此 最后一句代码prev->next 便会出现空指针解引用报错的问题
写法二的问题
tail->next 是空指针,因此 循环条件 tail->next->next空指针解引用报错
同样分类讨论:若只有一个节点(特点是 (*pphead)->next == NULL ),则直接释放这个节点,然后把头置成空指针即可
正确代码
尾删逻辑测试
⑥头删
由于释放头节点后,需要更新头节点,因此先定义一个next变量指向了第二个节点,再释放头节点,再更新头节点指针,链表为空直接断言,而下面这段代码适用只有一个节点的情况
头删逻辑测试
⑦查找数据域为x的节点,并返回该节点地址
1.不需要修改头节点,传一级指针即可
2.注意循环条件是 while(cur),而不是while(cur->next),否则遍历不到最后一个节点,若要查找的数据x在尾节点的数据域中,查找就会有误
⑧修改指定节点数据
第⑦点已经实现了查找功能,并可以通过接收返回值的形式拿到要修改节点的地址,因此我们直接修改即可
⑨任意位置插入
在pos位置之前插入
思路:定义prev指针,挨个遍历链表,找到pos前一个位置,创建新结点进行插入链接
测试结果:
插入成功了,然而这段代码是存在问题的,如果pos就是头节点指针呢?这段代码会如何呢?
可以发现,此时程序挂掉了,就是因为while循环无限下去了,因此我们应该单独处理pos为头节点地址的情况(调用头插函数)
正确代码
pos之前插入逻辑测试
在pos位置之后插入
在pos之后插入就比较简单了,此时就不需要传头指针了
有两种做法
第一种就是先链接新节点和pos位置之后的节点,再链接pos位置的节点和新节点(如果先链接pos节点和newNode节点,就找不到pos原本后面的节点了)
第二种就是再创建一个变量直接先保存pos下一个节点地址,然后链接顺序无所谓
法一
法二:
pos位置之后插入逻辑测试
⑩删除任意位置的节点
删除pos位置的节点
思路很简单,把当前节点(pos)释放后,需要把pos的前一个节点和pos的后一个节点链接起来,因此还是需要遍历找到pos的前一个节点
测试:
看似没有问题,但是若要删除的节点是头节点呢?也就是pos为头节点指针的时候,会怎样呢?
可以发现,程序挂掉了,仍旧是因为while陷入了死循环,因为我们需要分类讨论,若pos是头节点指针,直接调用头删即可
正确代码
删除pos位置节点逻辑测试
删除pos位置之后的节点
先保存pos之后位置节点的地址,然后释放pos位置之后的节点内存,同时链接,但是要注意的是删除pos位置之后的节点的前提是pos之后得有节点,因此断言要增加一点内容
删除pos位置之后节点逻辑测试
单链表的销毁
注意点就是在销毁当前节点之前,应该先保存下一个节点的地址,否则释放节点内存后就无法找到下一个节点了,最后再把plist置成空指针(*pphead = NULL)
好啦,本篇的单链表讲解就到此结束了,创作匆忙,仍有许多不足,感谢大家批评指正~~~