为什么要写单链表
之前我们写过了一个数据结构——顺序表,它可以在内存中连续的存放数据,但我们会发现,如果想要往其中插入一个数据,就必须要移动后面所有的数据,效率很低,于是就有了链表,链表在内存中是通过地址的方式来连接的,想要插入一个数据,不需要像顺序表那样复杂,只要将前一个节点的地址指向新的节点,然后将新的节点的地址指向下一个节点就可以了。具体实现我们看下面的代码部分。
代码实现部分
SList.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int SLDataType;
typedef struct SListNode
{
SLDataType x;
struct SListNode* next;
}SListNode;
//值传递,不需要对外面头结点进行改变
void SListPrint(SListNode* ps);
SListNode* SListFind(SListNode* phead, SLDataType x);
void SListInsertAfter(SListNode* pos, SLDataType x);
void SListEraseAfter(SListNode* pos);
int SListSize(SListNode* ps);
bool SListEmpty(SListNode* ps);
//地址传递,要对外面头结点进行改变
void SListPopBack(SListNode** ps);
void SListPushBack(SListNode** ps, SLDataType x);
void SListPopFront(SListNode** ps);
void SListPushFront(SListNode** ps, SLDataType x);
void SListInsert(SListNode** pphead, SListNode* pos, SLDataType x); //在某个节点之前插入
void SListErase(SListNode** ps, SListNode* pos); //删除pos位置
🍎 这一部分有几个要注意的点
1. 到底是传一级指针还是传二级指针?如果要改变传入的头节点(哪怕代码中只有一小部分要改变头结点,也要传地址),就要传地址,也就是二级指针,如果没有任何地方需要改变头结点,就值传递,也就是一级指针,这个点在下面的SList.c中也会具体讲到,因为我当时写代码时我就在这个点上错了好几次
2.
typedef struct SListNode
{
SLDataType x;
struct SListNode* next;
}SListNode;
结构体里面一定要写成struct SListNode这样的写法,不能写成SListNode,虽然你typedef自己定义的是这个类型,但这样就会造成一个先有鸡还是先有蛋的问题。
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//打印单聊表
void SListPrint(SListNode *ps)
{
while (ps)
{
printf("%d->", ps->x);
ps = ps->next;
}
printf("NULL\n");
}
//创建一个新的节点
SListNode* SListBuyNode(SLDataType x)
{
struct SListNode* n1 = (SListNode*)malloc(sizeof(SListNode));
if (n1 == NULL)
{
printf("creat fail\n");
exit(-1);
}
n1->x = x;
n1->next = NULL;
return n1;
}
//单链表尾删
void SListPopBack(SListNode** ps)
{
assert(ps);
assert(*ps);
SListNode* tail = *ps;
SListNode* prev =*ps;
//单链表一个节点的情况
if ((*ps)->next == NULL)
{
free(*ps);
*ps = NULL;
}
//单链表中有节点的情况
else
{
while (tail->next) //注意这儿和打印的地方不一样,这儿要这么写,具体情况要具体分析,自己举几个列子就知道了
{
prev = tail;
tail = tail->next;
}
free(tail);
/*tail = NULL;*/
prev->next = NULL;
}
}
//单链表尾插
void SListPushBack(SListNode** ps, SLDataType x)
{
assert(ps);
SListNode* tail = *ps;
//没有节点的情况
if (*ps == NULL)
{
*ps = SListBuyNode(x);
}
//有节点的情况
else
{
while (tail->next)
{
tail = tail->next;
}
tail->next = SListBuyNode(x);
}
}
//头删
void SListPopFront(SListNode** ps)
{
assert(ps);
assert(*ps);
//一个节点的情况
if ((*ps)->next == NULL)
{
free(*ps);
*ps = NULL;
}
//多个节点的情况
else
{
SListNode* next = (*ps)->next;
free(*ps);
*ps = next;
}
}
void SListPushFront(SListNode** ps, SLDataType x)
{
assert(ps);
//空节点的情况
if (*ps == NULL)
{
*ps = SListBuyNode(x);
}
//有节点的情况
else
{
SListNode* temp = *ps;
*ps = SListBuyNode(x);
(*ps)->next = temp;
}
//这样写也可以,因为if里面的情况包括在了else的情况里面,这样写可以减少代码的重复,但上面一种写法看起来更加的简洁明了
//SListNode* temp = *ps;
//*ps = SListBuyNode(x);
//(*ps)->next = temp;
}
SListNode* SListFind(SListNode* phead, SLDataType x)
{
SListNode* cur = phead;
while (cur) //这儿一定要写cur,不能写cur->next,不然最后一个节点是进不去的
{
if (cur->x == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
//在某个节点之前插入
void SListInsert(SListNode** ps, SListNode* pos, SLDataType x)
{
assert(ps);
SListNode* tail = *ps;
SListNode* prev = *ps;
//空节点的情况
if (*ps == NULL) //注意这儿千万不能写成tail,然后对tail进行操作
{
*ps = SListBuyNode(x);
}
//一个节点的情况
else if ((tail)->next == NULL)
{
struct SListNode* newnode = SListBuyNode(x);
*ps = newnode;
newnode->next = tail;
}
//多个节点的情况
else
{
while ((tail) != pos)
{
prev = tail;
tail = tail->next;
}
/*prev->next = SListBuyNode(x);
prev->next->next = tail; */ //因为前面的SListBuyNode是你自己创建的一块空间,这块空间不与原链表完整的连接在一起,所以这句代码会出问题
struct SListNode* newnode = SListBuyNode(x);
newnode->next = tail;
prev->next = newnode;
}
}
//void SListInsert(SListNode** pphead, SListNode* pos, SLDataType x)
//{
// assert(pphead);
// assert(pos);
//
// // 1、头插
// // 2、后面插入
// if (*pphead == pos)
// {
// SListPushFront(pphead, x);
// }
// else
// {
// // 找到pos位置的前一个节点
// SListNode* prev = *pphead;
// while (prev->next != pos)
// {
// prev = prev->next;
// }
//
// SListNode* newnode = SListBuyNode(x);
// newnode->next = pos;
// prev->next = newnode;
// }
//}
//在某个节点之后插入
void SListInsertAfter( SListNode* pos, SLDataType x)
{
SListNode* newnode = SListBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//在某个位置之后删除
void SListEraseAfter(SListNode* pos)
{
SListNode* nextnext = pos->next->next;
free(pos->next);
pos->next = nextnext;
}
//删除pos这个位置
void SListErase(SListNode** ps,SListNode* pos)
{
assert(ps);
assert(*ps);
//一个节点的情况 ,因为这个时候头结点变化了,所以要用2级指针传参,而且不能像前面一样,把*ps赋值给tail然后对tail进行操作了,因为那样tail是影响不到外面的
if ((*ps)->next == NULL)
{
free(*ps);
*ps = NULL;
}
//多个节点的情况
else
{
SListNode* cur = *ps; //这儿也是,不可以对*ps直接用,要保护好*ps的位置,因为你不需要对头进行改变,如果用*ps的话,那么外面也会接着变的
SListNode *prev = *ps;
while (cur!= pos)
{
prev = cur;
cur = cur->next;
}
SListNode *next = cur->next;
free(cur);
prev->next = next;
}
}
//计算有多少个节点
int SListSize(SListNode* ps)
{
int num = 0;
while (ps != NULL)
{
num++;
ps = ps->next;
}
return num;
}
//判断是否为空
bool SListEmpty(SListNode* ps)
{
return ps == NULL;
}
🥑这儿就重点讲几个写的时候觉得要注意的版块以及一些要注意的点
1. 注意while里面到底是写ps,还是ps->next来作为判断,这里就要看你的需求了,最好自己举几个列子代进去分析.
2.到底能不能对ps解引用或者说到底这段代码传参时要不要传二级指针,这是很重要的一个注意点,下面我们来对比几段代码
void SListPopBack(SListNode** ps)
{
assert(ps);
assert(*ps);
SListNode* tail = *ps;
SListNode* prev =*ps;
//单链表一个节点的情况
if ((*ps)->next == NULL)
{
free(*ps);
*ps = NULL;
}
//单链表中有节点的情况
else
{
while (tail->next) //注意这儿和打印的地方不一样,这儿要这么写,具体情况要具体分析,自己举几个列子就知道了
{
prev = tail;
tail = tail->next;
}
free(tail);
/*tail = NULL;*/
prev->next = NULL;
}
}
可以看到这个代码在一个节点时对ps解引用了,又因为是二级指针,所以改变ps也就对外面的头节点进行了改变,所以这儿是要传二级指针的,因为你对代码中对头节点发生了改变,再看else中的代码,我们会发现这里面会没有ps, 而是在代码最前面把ps赋值给了tail,然后改变tail,是就相当于是值传递了,无论tail怎么变ps都是不会变的,ps不变,外面的头节点也不会变,这不正是我们想要的吗,因为这链表中有节点的情况下,我们不希望改变ps,也就是头节点,所以万万不能写成*ps ,一开始我写的时候并没有注意到这个点,下面来看下我们当时是怎么犯下这个错误的。
//删除pos这个位置
void SListErase(SListNode** ps,SListNode* pos)
{
assert(ps);
assert(*ps);
//一个节点的情况 ,因为这个时候头结点变化了,所以要用2级指针传参,而且不能像前面一样,把*ps赋值给tail然后对tail进行操作了,因为那样tail是影响不到外面的
if ((*ps)->next == NULL)
{
free(*ps);
*ps = NULL;
}
//多个节点的情况
else
{
SListNode *prev = *ps;
while (*ps!= pos)
{
prev = *ps;
*ps= (*ps)->next;
}
SListNode *next = (*ps)->next;
free(*ps);
prev->next = next;
}
}
写完之后运行会发现输出一串随机值,而且后面的节点都没了
出现这种情况的原因正是因为在这个代码中,你不需要改变头节点,而你偏偏解引用了ps,且这个是地址传递,会影响到外面,因此头节点一直被你窜改,也就不知道指向哪里了,所以会有随机值,这里可以把*ps赋值给另外一个指针,又因为是值传递,所以那个指针的改变并不会影响头节点
3. 连续写两个next时一定要注意,万一出现问题,至于为什么,在代码注释中写明了
//在某个节点之前插入
void SListInsert(SListNode** ps, SListNode* pos, SLDataType x)
{
assert(ps);
SListNode* tail = *ps;
SListNode* prev = *ps;
//空节点的情况
if (*ps == NULL) //注意这儿千万不能写成tail,然后对tail进行操作
{
*ps = SListBuyNode(x);
}
//一个节点的情况
else if ((tail)->next == NULL)
{
struct SListNode* newnode = SListBuyNode(x);
*ps = newnode;
newnode->next = tail;
}
//多个节点的情况
else
{
while ((tail) != pos)
{
prev = tail;
tail = tail->next;
}
/*prev->next = SListBuyNode(x);
prev->next->next = tail; */ //因为前面的SListBuyNode是你自己创建的一块空间,这块空间不与原链表完整的连接在一起,所以这句代码会出问题
struct SListNode* newnode = SListBuyNode(x);
newnode->next = tail;
prev->next = newnode;
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//void test1()
//{
// SListNode *n1 = (SListNode*)malloc(sizeof(SListNode));
// n1->x = 1;
// SListNode *n2 = (SListNode*)malloc(sizeof(SListNode));
// n2->x = 2;
// SListNode *n3= (SListNode*)malloc(sizeof(SListNode));
// n3->x =3;
// n1->next = n2;
// n2->next = n3;
// n3->next = NULL;
// SListPrint(n1);
// SListPopBack(&n1);
// SListPrint(n1);
// SListPopBack(&n1);
// SListPrint(n1);
//}
void test2()
{
struct SListNode* ps = NULL;
SListPushBack(&ps, 1);
SListPushBack(&ps, 2);
SListPushBack(&ps, 3);
SListPushBack(&ps, 4);
SListPrint(ps);
SListPopBack(&ps);
SListPrint(ps);
SListPopBack(&ps);
SListPrint(ps);
SListPopFront(&ps);
SListPrint(ps);
SListPushFront(&ps, 1);
SListPrint(ps);
SListNode* pos = SListFind(ps, 2);
SListInsert(&ps,pos,4);
SListPrint(ps);
pos = SListFind(ps, 2);
SListInsertAfter(pos, 4);
SListPrint(ps);
pos = SListFind(ps, 2);
SListEraseAfter(pos);
SListPrint(ps);
pos = SListFind(ps, 4);
SListErase(&ps,pos);
SListPrint(ps);
printf("%d\n", SListSize(ps));
printf("%d\n", SListEmpty(ps));
}
int main()
{
/*test1();*/
test2();
return 0;
}
💡至于测试部分要注意的
1.要注意是传地址还是传值,如果要改变外面的头节点的话就传地址,不要的话就传值,这一点在上面也强调过了,这里再重复强调下.
2. 要在某个位置删除或插入某个数据时,一定要先通过SListFind这个函数找到你要删除或插入的数据的那个位置的结构体,然后传结构体过去,而不是傻乎乎的一直传你要删除或插入的数据,以表明那个位置。
总结和思考
这次写单链表并没有上次写顺序表那么顺利,主要是要不要解引用ps,也就是头指针变化是要ps,传地址,不需要改变头指针时,万万不能ps,对其随便改变,以后遇到这种类似的情况一定要相当注意;其次是命名方面,大部分命名还可以,但也有不足,比如ps这个就不好,可以命名成phead,这样更加提醒自己是头节点,同时这样命名也更明了。总的来说,单链表在逻辑上并不难,细节很多,可能随着更多的学习后会对单链表有一个更深入的理解.