简单实现单链表

序言

上一篇博客我总结了顺序表,顺序表是最基础的一个数据结构,其主要的底层逻辑就是数组,但是,他的缺陷也很明显。比如,在空间的开辟的时候,随着数据存储的越大,他所开辟的空间就会成倍数增长,最终导致较多空间的浪费;在插入数据的时候,每次都需要把数据往后进行挪动,如果数据是千万级的,那么其效率就会大大降低。因此链表的作用,就随着而来。

1.万物之基——结构体

对于链表来说,他的物理结构,就是一个接一个的节点通过指针来串联起来。那么链表中的节点又是什么?其实,链表中节点的本质就是一个个结构体,结构体里面存放了两个变量,一个是该节点的值,另一个是下一个节点的地址,因为要存放下一个节点(结构体)的地址,因而在创建节点的时候,是需要创建一个结构体的指针变量来进行存储的,结构体指针里既可以存放值,也可以存放地址。

以下是创建节点的代码:

typedef int SLTDataType;

typedef struct SLTist
{
	SLTDataType x;
	struct Slist* next;
}SLT;

在这段代码中,首先由于我们不知道未来节点(结构体)里要存放的是什么类型的变量,在这个地方,我们可以使用typedef来给我们要存放的变量进行重命名,在未来修改的时候,将会方便许多。而第二个变量就很明显,是一个结构体的指针,他将存放下一个节点(结构体)的地址。

2.链表中节点的创建

其实在链表里面,每一个节点都是由malloc函数进行动态内存的开辟得来的,因此,对于链表数据的存储,我们可以做到按需来开辟,也就是说,有多少需求,就可以创造多少个空间节点。因此,相较于顺序表,链表对于空间利用的优势,就在这里展现出来了。

代码如下:

SLT* SLTListBuyNode(SLTDataType x)
{
	SLT* node = (SLT*)malloc(sizeof(SLT));
	node->x = x;
	node->next = NULL;
	return node;
}

在这个代码中,我们最终的目标是要创建这个节点,因此创建完之后,是要返回这个节点的指针的。以方便后续的调用。

3.链表的尾插

要如何实现链表的尾插?

对于实现链表的尾插,首先要分清楚两种情况,一种是当前链表里面没有数据;第二种是链表里面有数据。

1.链表里面没有数据

假设我们要传进链表里面的是node节点。当链表里面没有数据的时候,这也就意味着传进尾插函数的指针当前为空指针,这个时候就没必要把空指针->next=node了,因为此时的指针为空,空的下一个仍然是空,这就会导致访问错误,无法找到空的next,vs系统会报错这是一个nullptr

因此正确的做法是,直接把头指针赋给node,然后返回空就行了。

2.链表里面有数据

如果链表里面是有数据的话,这个时候就需要通过遍历链表来进行找尾,找到链表的尾部,然后用尾节点的next指向node,再用node的next指向尾节点的next(当然,这个地方也可以说用node的next指向NULL,所表达的意思都是一样的)。

在这个地方需要注意一下的是,链表的遍历和顺序表的遍历是非常不一样的。

在顺序表中,因为顺序表的本质仍然是数组,是可以通过下标来进

行访问的,因此是可以通过for循环来对顺序表进行遍历。

但是在链表里面,是有两种遍历的方式,第一种的遍历条件是node->next!=NULL,这种一般都会出现在找尾的场景下使用,第二种的遍历条件是node!=NULL,这种是在要寻找一个节点的场景下使用。

而在尾插的函数里面,是需要找到尾节点的位置,因而是需要用node->next!=NULL这个条件来加入循环中寻找。

那具体该如何去进行循环?思路就是新建一个结构体指针让他指向头节点,让这个新建的结构体指针来进行遍历。

void SLTPushBack(SLT** pphead, SLTDataType x)
{
	SLT* node = SLTListBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = node;
		return;
	}
	if (*pphead != NULL)
	{
		SLT* pcur = (SLT*)malloc(sizeof(SLT));
		pcur = *pphead;
		while (pcur->next != NULL)
		{
			pcur = pcur->next;
		}
		pcur->next = node;
	}
}

4.链表的头插

而对于头插而言,与尾插一样,也是需要判断链表里面是否是存在数据。

1.链表里面没有数据

如果链表里面没有数据,则直接把头指针赋给node就行了。

2.链表里面有数据

如果链表里面有数据,其操作的复杂程度还远不如尾插,整体思路就是把node->next指向头节点,然后再把头节点赋给node这个节点,使它称为新的头节点。

以下是代码:

void SLTPushFront(SLT** pphead, SLTDataType x)
{
	SLT* node = SLTListBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = node;
		return;
	}
	if (*pphead != NULL)
	{
		SLT* pcur = (SLT*)malloc(sizeof(SLT));
		pcur = *pphead;
		node->next = pcur;
		*pphead = node;
	}
}

5.链表的尾删

尾删的思路也是非常的简单,首先找尾,然后释放。首先定义一个新的结构体指针,让他指向头节点,然后,通过这个新定义的结构体指针来进行对链表的遍历,前面我们说过,在链表的遍历中,如果想找尾,则可以利用node->next!=NULL这个条件来进行遍历的,当node->next为空的时候,则说明我们已经来到了链表的尾部。但是来到了这里,我们还不能直接把尾节点给释放掉,因为如果直接给释放掉,那么尾节点的前一个节点的next,就找不到了,他将指向的是一个未知的,混乱的东西。因为free的本质是把malloc等函数所开辟的空间给释放掉,是释放空间,并不是释放指针,空间都被释放掉了,那前一个节点的next又将指向什么东西呢?

因此,在释放掉尾节点之前,我们应该先处理好尾节点的前一个节点的next所指向的地方。因为在前面的创建节点的时候,我们把每一个节点的next都指向了NULL,如果有其它值,将会把这个NULL给覆盖掉,如果没有,则这个next仍然为空。

因此,在链表中,尾节点的next的指向是为空的,这个时候,我们只需要把前一个节点的next指向尾节点的next,然后再把尾节点给覆盖掉就可以了。

void SLTPopBack(SLT** pphead)
{
	assert(*pphead);	
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	SLT* ppre = NULL;
	SLT* pTali = *pphead;
	while (pTali->next!=NULL)
	{
		ppre = pTali;
		pTali = pTali->next;
	}
	ppre->next = pTali->next;
	free(pTali);
	pTali = NULL;
}

在这个代码里面,值得一提的是,我在这定义了一个ppre的结构体指针,为什么要这么干?

因为如果不用他来记录pTail所走过的位置,那么即使找到尾,那也无法再找到尾节点的前一个节点了,这就是单链表的缺陷。而在代码里,pTail永远比ppre多走一步,当pTail停的时候,ppre所指向的刚好是pTail的前一个节点,这样,就方便的很多。

而assert这个函数的设计,是一种较为暴力的判断,如果assert里面的条件为假,则就触发判断,强制程序中断。在这个代码中,assert(*pphead)也可以写成assert(*pphead!=NULL),这两者的意思都是判断传过来的指针是否为空指针,如果是空,那么这个删除操作也就没有必要进行了。

6.链表的头删

头删的操作远远比尾删更简单,就是首先创建一个另外结构体指针变量pcur,把他的值指向链表的头节点,然后把指向头节点原来的指针向后挪动一位,是他指向自己的next,然后再把pcur给释放掉。

代码如下:
 

void SLTPopFornt(SLT** pphead)
{
	assert(*pphead);
	SLT* pcur = *pphead;
	*pphead = (*pphead)->next;
	free(pcur);
	pcur = NULL;
}

这里面需要注意的是,*pphead = (*pphead)->next;这行代码中,为什么在右边需要对*pphead括起来?不括会怎样?

报错~

为什么?因为在c语言中,->操作符的优先级比*号要高,我们想要的是先把pphead解引用之后再对里面进行取值,但是由于->操作符的优先级比*号要高,因此他首先对pphead进行取值,但pphead并不是一个结构体,所以就取值失败。因此要用括号把*pphead括起来进行解引用。

7.在链表里面寻找节点(单纯寻找)

这个就太简单了,除了循环条件那需要注意一下。前面说过,node!=NULL,这种是在要寻找一个节点的场景下使用。

不想多说,直接上代码

void SLTFindNode(SLT** pphead, SLTDataType x)
{
	SLT* find = *pphead;
	while (find->next != NULL)
	{
		if (find->x == x)
		{
			printf("找到了,是%d", find->x);
		}
		find = find->next;
	}
	printf("找不到\n");
}

8.在指定位置的前面插入数据

1.什么是"指定位置"?

在实现这个方法之前,我们首先来探讨一下,这个"指定位置"指的是什么意思,是一个节点?还是第几个节点?如果是前者,那么就是一个结构体指针变量;如果是后者,则是一个整型变量。

实际上,上面的两种说法都可以是"指定位置"。但为了程序的效率以及可读性,在这里,就使用前者这个概念。

如果采用了前者这个方案,那么首先就要在链表中找到这个节点,就需要定义一个专门来寻找这个节点的函数,其返回值就是结构体指针,也就是返回一个节点。

那怎么实现呢?首先我们可以把想要查找的值给传进去,注意,是值,而不是节点,然后在链表里面进行遍历,如果发现了某一个节点中有这样的一个值,那么就返回这个节点。而未来这个返回的节点,将在实现"指定位置前插入数据"中起到重要的作用。

代码如下:

SLT* SLTListFind(SLT** pphead, SLTDataType x)
{
	assert(*pphead);
	SLT* node = *pphead;
	while (node)
	{
		if (node->x == x)
		{
			return node;
		}
		node = node->next;
	}
	return NULL;
}

解决了寻找节点的问题,接下来就可以进行插入操作了。

由于我们这里是想在"指定位置之前"插入数据,因此,我们的遍历条件可以是node->next!=pos,如果发现了是相等,那么就找到了这个节点,此时此刻的node->next正好是位于pos的前面,这个时候的node->next就可以指向即将插入进来的新节点,新节点的next就可以指向pos

代码如下:

void SLTInsertFrontPos(SLT** pphead, SLT* pos, SLTDataType x)
{
	assert(pos);
	assert(*pphead);
	SLT* node = SLTListBuyNode(x);
	if ((*pphead)->next==NULL)
	{
		node->next = *pphead;
		*pphead = node;
	}
	SLT* pcur = *pphead;
	while (pcur->next != pos)
	{
		pcur = pcur->next;
	}
	node->next = pos;
	pcur->next = node;
}

9.链表的打印

对于链表的打印,我想都到这里了,估计也是差不多明白他的循环条件了吧?

直接上代码:

void SLTPrintf(SLT** pphead)
{
	SLT* node = *pphead;
	while (node)
	{
		printf("%d->", node->x);
		node = node->next;
	}
	printf("\n");
}

10.对于一些细节问题的深究

1.关于头删和尾删,他们为什么能删掉节点?

在我写的代码里面可以很清楚的看到,当我想删掉一个节点的时候,我通常会创建一个新的指针,让那个指针去遍历整个链表,然后释放,以达到我想要删除某个数据的目的。

就拿头删来距离,我在这里释放掉的明明是pcur呀,为什么会把*pphead给释放掉了?

从代码的表面上来看,STL*pcur = *pphead就好像是一次赋值,只是把*pphead的值赋给了*pcur,我把*pcur给释放掉了,跟*pphead有什么关系?

这个问题一开始对于我这种基础不太好的人而言,也是感到非常地困惑的。

后来我通过调试发现,当free这条语句释放掉之后,*pcur的空间和*pphead的空间都发生了改变

后来就想明白了,可以说,我所创建的*pcur他仅仅是一个指针

这个指针可以叫任何的名字,他们都共同指向一个节点,当执行free这条语句的时候,他释放的是指针所指向的结构体空间,在这里,指针仅仅是一个媒介,通过这个媒介,free语句才能释放掉该释放的空间,并不是释放指针这么简单。也并不是简单的赋值。

2.为什么要传二级指针?传一级指针不行吗?

对于这个问题,我们首先要明白,链表,他是环环相扣的,他每一个节点里面,都存放着下一个节点的地址,着也就意味着,他并不是一个简单的结构体能存放的,必须得创建结构体指针来存放他。

此时创建的是一个结构体的指针,那么如果你想改变一个指针的值,首先就要传这个指针的地址,那么你就需要用到一个变量来存放这个指针的地址,这个时候就需要用到二级指针来接收了。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值