单链表的实现

为什么要写单链表

之前我们写过了一个数据结构——顺序表,它可以在内存中连续的存放数据,但我们会发现,如果想要往其中插入一个数据,就必须要移动后面所有的数据,效率很低,于是就有了链表,链表在内存中是通过地址的方式来连接的,想要插入一个数据,不需要像顺序表那样复杂,只要将前一个节点的地址指向新的节点,然后将新的节点的地址指向下一个节点就可以了。具体实现我们看下面的代码部分。

代码实现部分

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,这样更加提醒自己是头节点,同时这样命名也更明了。总的来说,单链表在逻辑上并不难,细节很多,可能随着更多的学习后会对单链表有一个更深入的理解.

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个数学不怎么好的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值