一篇文章讲透单链表!!!

点击此处访问裤裤的数据结构专栏

目录

一.什么是链表

二.链表的分类

三.单链表的定义

四.单链表的功能

五.单链表功能实现

5.0创建结点

5.1打印单链表

5.2单链表头插

5.3单链表头删

5.4单链表尾插-->优先讲

5.5单链表尾删

5.6单链表查找

5.7单链表指定位置插入

5.7.1指定位置之前插入

 5.7.2指定位置之后插入

5.8单链表任意位置删除

5.8.1删除pos位置结点

5.8.2删除pos结点的下一个结点

5.9销毁单链表


一.什么是链表

链表是一类物理存储结构上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

物理存储结构:真实的在内存中的存储结构。

逻辑存储结构:脑袋中构思的存储结构。

二.链表的分类

链表可以根据单向或者双向、带头或者不带头、循环或者不循环分出2^3==8种结构。

其中最常见的结构是单向不带头不循环链表和双向带头循环链表。

我们分篇讲解单向链表和双向带头链表。

等我们学到单链表的定义之后,我再展开给大家讲述这里面的一些名词的具体意思。

三.单链表的定义

我们刚刚已经介绍了链表是由指针依次链接形成的一种存储数据的结构。

那么,链表里面应该有一个成员是数据,一个成员是指针。

现在我先给大家定义一个单链表

typedef struct SListNode {
	SLTDataType data;//数据
	struct SListNode* next;//指向下一个数据的指针
}SLTNode;

那么,我们可以画出下图,它的结构应是如此:

现在再给大家解释一下链表的分类

单向or双向:单向是定义一个指针指向下一个链表块,双向是定义两个指针,一个指针指向下一个链表块,一个指针指向上一个链表块。

带头or没头:头结点又称为哨兵位,带不带头结点就是带不带哨兵位。哨兵位不保存数据。

循环or不循环:在上图的双向链表种,可以看到首结点的pre指向空,尾结点的next结点指向NULL,如果让首结点的pre指向尾结点,让尾结点的next指向首结点,那么这个链表就循环起来了。

四.单链表的功能

单链表的功能如下:

  1. 创建结点(其实这个不是一个功能,但是我们要在实现下面的功能时使用它)
  2. 打印单链表中的数据。
  3. 对单链表进行头插(开头插入数据)。
  4. 对单链表进行头删(开头删除数据)。
  5. 对单链表进行尾插(末尾插入数据)。
  6. 对单链表进行尾删(末尾删除数据)。
  7. 对单链表进行查找数据。
  8. 对单链表数据进行修改。
  9. 任意位置的删除和插入数据。
  10. 销毁单链表

五.单链表功能实现

5.0创建结点

我们在完成增删查改的功能之前首先要创建结点,因此我们要写的第一个函数是创建结点。

我们创建的结点是在堆上创建的,要把地址返回回来,我们才能够使用。 

这里使用的malloc函数在这里有详细解释   点击这里进入动态内存管理函数

//结点创建之后要返回指针变量.
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//讲解逻辑取反运算符
	if (!newnode)
	{
		perror("malloc fail");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

5.1打印单链表

由于我们的链表是由多个结点构成的,所以打印单链表就要将所有的结点打印出来。

由于我们的单链表是线性结构的,节点之间是相连的,因此我们可以通过遍历单链表的方法来完成这个函数。所以我们要将单链表的首结点的地址作为参数传递。

但是我们在函数使用的过程种不能改变首结点的地址,因此我们需要创建一个新的结构体指针指向首结点,我们后续改变我们创建的指针变量即可。

而最后一个指针指向空,但while循环种当pcur==NULL时就会退出循环。所以我们要在while循环外部单独写一个语句打印NULL。

//打印
void SLTPrint(SLTNode* phead)
{
	//这里要创建一个pcur结点
	//如果直接修改phead结点,那么我们的phead结点就变了。
	SLTNode* pcur = phead;
	while (pcur)//pcur!=NULL
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

5.2单链表头插

单链表的头插即在第一个结点之前插入一个结点。

首先,插入一个结点,我们应创建一个结点。

这里我们就要考虑两种情况

   1.这个链表是个空链表

      这时我们就需要单独判断一下这个链表是否为空,如果为空的话,我们让第一个结点的          地址等于我们创建的结点的地址即可

   2.这个链表是个非空链表

      这时我们直接让创建的结点的next指针指向第一个结点即可。

因此我们可以写出如下代码:

void SLTPushFront1(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	//*pphead是第一个结点的地址。
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		newnode->next = *pphead;
	//更新头结点
	*pphead = newnode;
	}

}

 但是我们发现第一种情况和第二种情况种都需要更新头结点的代码,即*pphead=newnode;

 这样显得代码过于冗余,我们可以做出如下代码的更改

void SLTPushFront2(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	//*pphead是第一个结点的地址。
	newnode->next = *pphead;
	//更新头结点
	*pphead = newnode;
}

在这里,我们删去了if-else的判断结构,那么这段代码是否能够满足需求呢?

当没有结点时,我们会先将newnode的next指针置空,然后将头结点的地址更新为newnode的地址

当有结点时,这段代码和第一个头插代码相同,这里不再阐述。

5.3单链表头删

单链表的头删,即删除单链表的第一个结点。

这里我们也要分两种情况考虑:

          1.这个链表中只有一个结点-->这时我们需要删掉这个结点,然后将头节点置空

          2.这个链表中有好多个结点->先创建一个指针指向头节点的下一个结点,然后删掉头结点,            并更新头节点即可。

因此我们可以写出如下代码

void SLTPopFront2(SLTNode** pphead)
{
	//没有结点
	assert(pphead && *pphead);
	//一个结点
	if ((*pphead)->next = NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* next = (*pphead)->next;
		free(*pphead);
		*pphead = next;
	}
}

但是,在只有一个结点的情况下,第一个结点的next指针一定为空;我们将第一个结点的next指针赋给新的头结点,也就将头结点置空了。因此上述代码可以改为下面这段代码: 

void SLTPopFront1(SLTNode** pphead)
{
	//没有结点
	assert(pphead && *pphead);
	//一个结点and多个结点
	SLTNode* next=(*pphead)->next;
	free(*pphead);
	*pphead = next;
}

5.4单链表尾插-->优先讲

单链表的尾插,即在单链表的尾结点后插入一个结点。

在这里我们也要分两种情况:

1.如果链表中没有结点的话,那么我们直接让首结点等于我们创建的结点即可。

2.如果链表中有结点的话,我们首先要找到尾结点,这时就需要我们定义一个指针去寻找尾结点。

然后让尾结点的next指针等于我们的新结点即可。

//尾插-->空链表和非空链表
//为空时,要修改首结点地址(*phead),所以要传二级指针(指向首结点的指针的地址)
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{  
	assert(pphead);
	//pphead是一个二级指针,它保存的是一级指针的地址
	//*pphead,就是将二级指针的内容解引用出来,也就是一级指针的地址
	//而一级指针的内容是指向的空间的地址
	//因此,结论是*pphead是一个地址.
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		//newnode-->地址  *newnode-->内容
		//我们要指向newnode的地址,而不是内容
		//因此,这里是newnode,而不是*newnode
		*pphead = newnode;
	}
	else
	{
		//找尾
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//ptail指向的就是尾结点
		ptail->next = newnode;
	}
}

5.5单链表尾删

单链表的尾删,首先要保证链表不能为空。

之后,如果只有一个结点,我们直接free掉这个结点并置空即可。

如果有多个结点的话,我们就需要找尾结点,同样的,我们在这里定义一个指针去寻找尾结点。

当我们找到尾结点之后,我们就可以free并置空尾结点了。 

//尾删
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	//只有一个结点
	if ((*pphead)->next == NULL)//->的优先级高于*
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个结点
	else
	{
		//找尾
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		//如何更新结点?
	}
}

但是,我们发现了一个问题,倒数第二个结点的next指针指向的还是原来的尾结点,而不是空。

但,它应该置空!

因此,我们就还需要一个指针来记录倒数第二个结点,并用这个指针来更新尾结点

//尾删
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead&&*pphead);
	//只有一个结点
	if ((*pphead)->next == NULL)//->的优先级高于*
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个结点
	else
	{
		//找尾
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail;//更新结点。
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		prev->next = NULL;//更新结点
	}
}

5.6单链表查找

 单链表的查找,即查找一个值是否在这个链表中

我们还是按照刚刚的方式遍历单链表,然后返回结点的地址即可。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;//这里讲解一下return语句
		}
		//else
		//{
		//	pcur = pcur->next;
		//}
		pcur = pcur->next;
	}
	return NULL;
}

 如果我们每次都使用if  else循环未免过于繁琐。反正找到了值会在在if语句中返回,返回了也自然不会执行下面的语句了。我们将else中的语句单独写出来也自然是可以的了。

5.7单链表指定位置插入

5.7.1指定位置之前插入

首先我们要判断传的二级指针是否为空以及链表是否有结点。

其次要判断pos是不是一个有效值

之后就可以插入数据了。

如果我们在第一个结点之前插入结点,那就是头插,我们直接调用头插函数即可。

下面就是我们分析的重头戏了!

如果不是在第一个结点之前插入结点。

那么这个结点一定会改变两个结点

一个是pos结点的前一个结点的next指针

另一个是newnode指针的next指针

如图所示:

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	//在第一个之前插入-->头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	//在第一个之后插入数据
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}

 5.7.2指定位置之后插入

在指针的位置之后插入,我们也需要改变两个指针。

一个是pos的next指针,另一个是newnode的next指针。

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode =SLTBuyNode(x);
	//以下两行不能互换位置,课上讲解一下。
	newnode->next = pos->next;
	pos->next = newnode;
}

5.8单链表任意位置删除

5.8.1删除pos位置结点

如果pos是头结点,即头删,直接调用头删函数即可。

 如果pos不是头结点,那么我们删除pos结点,就一定要先改变pos结点的前一个结点。

因此我们需要定义一个指针来通过遍历找到pos结点的前一个结点。

之后将prev的next指针改为pos结点的next指针,之后将pos的next指针置空即可。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	if (pos == *pphead)
	{
		//头删
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

5.8.2删除pos结点的下一个结点

首先,我们要确保pos结点有下一个结点。

之后我们要删除这个结点,如图所示。

//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

5.9销毁单链表

因为我们的单链表的结点是一个一个申请的,因此也需要我们一个一个删除。

 因此我们就可以遍历删除,但是最后要置空。

//销毁链表
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;//这里不能写pcur=NULL
	}
	pcur = NULL;
}

Slist.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode {
	SLTDataType data;//数据
	struct SListNode* next;//指向下一个数据的指针
}SLTNode;

//打印
void SLTPrint(SLTNode* phead);
//结点创建
SLTNode* SLTBuyNode(SLTDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

  • 35
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值