详解单链表(内有精美图示哦)

全文目录

  • 引言
  • 链表
    • 链表的定义与结构
    • 链表的分类
  • 单链表的实现及对数据的操作
    • 单链表的创建与销毁
      • 创建
      • 销毁
    • 单链表的打印
    • 单链表的头插与头删
      • 头插
      • 头删
    • 单链表的尾插与尾删
      • 尾插
      • 尾删
    • 单链表的查找
    • 单链表在pos位置后插入/删除
      • 插入
      • 删除
    • 单链表在pos位置插入/删除
      • 插入
      • 删除
  • 总结

引言

在上一篇文章中,我们了解了顺序表的相关知识,并且实现了用顺序表管理数据。

但在这过程中,我们发现了使用顺序表管理数据时,其实是存在一些不方便的:
比如当存储空间已经被扩容的时候,删除了许多的数据,就会导致大片的内存浪费;
比如当我们需要扩容时,可能会异地扩容,这个过程会比较影响效率;
再比如当我们需要在顺序表前面插入数据时,过程会比较麻烦。

相对于顺序表,同属线性表的链表在这些方面就有着比较好的表现。

链表也有许多不同的种类:单向或双向链表、带头或不带头的链表、循环或非循环的链表等。在接下来的几篇篇文章中就会详细介绍链表的相关知识:

链表

链表的定义与结构

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

也就是说,链表是逻辑上连续,但存储结构上不连续的线性结构。我们可以通过指针访问到链表中的下一个元素。所以,在一个链表的结点中,应该至少包含两个元素:当前结点的数据与指向下一个结点的指针。

这就需要我们用到结构体的知识:我们在学习结构体时介绍过结构体的自引用,即结构体中的一个成员类型是结构体指针。即,我们可以将链表结点的类型定义为(当然,这是最简单的形式,只能依次访问链表的元素):

typedef int SLTDateType;

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

链表的分类

链表有许多的类型:带头与不带头、循环与非循环、单向与双向:

详解单链表(内有精美图示哦)_第1张图片
详解单链表(内有精美图示哦)_第2张图片
详解单链表(内有精美图示哦)_第3张图片
当然,这些种类之间有许多的组合方式,根据需要,可以定义各种各样的链表。

其中,最为简单的就是无头单向非循环链表,这也是此篇文章介绍的重点:

单链表的实现及对数据的操作

对于单链表结点的结构,与上面我们介绍的栗子相同,就是最为简单的类型:
包括当前结点的数据与指向下一个节点的结构体指针:

typedef int SLTDateType;

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

与学习顺序表时类似,我们可以实现一下用单链表来管理数据。同样的,这些功能的实现都将封装为函数:

在这之前,我们首先需要定义一个结构体指针plist,用于访问单链表的第一个结点。这个指针被初始化为NULL:

SListNode* plist = NULL;

单链表的创建与销毁

创建

在开辟顺序表的空间时,我们可以直接动态申请一块连续的空间来存放一些数据。但是对于单链表而言,它在物理空间上不是连续的。所以当我们为单链表开辟空间时,就需要一个结点一个结点分别开辟空间。

首先我们需要一块大小为结构体大小的空间,这块空间可以动态开辟:

SListNode* plist = (SListNode*)malloc(sizeof(SListNode));

在开辟某一个空间后,我们需要将这块空间初始化:将该结点的data成员初始化为想要存储的数据(这个数据可以作为参数传给函数),然后将这个结点的next成员初始化为NULL。

最后,返回已经成功创建的结点的结构体指针:

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* plist = (SListNode*)malloc(sizeof(SListNode));
	if (plist == NULL)
	{
		perror("malloc");
		return NULL;
	}
	plist->data = x;
	plist->next = NULL;
	return plist;
}

单链表的结点是动态开辟的,当然需要将这些空间依次释放掉,以免出现内存泄漏的问题。

销毁

当依次销毁单链表的每一个节点时,我们需要两个指针变量cur与aftercur。通过这两个指针变量,aftercur可以在cur被销毁前记录cur->next的值,从而实现在销毁cur指向的空间后,可以通过aftercur访问到下一个空间而继续进行销毁操作。
依次循环,当cur为NULL时终止,实现销毁每一个结点:
详解单链表(内有精美图示哦)_第4张图片

// 单链表的销毁
void SListDestroy(SListNode* plist)
{
	SListNode* cur = plist;
	SListNode* aftercur = cur->next;
	while (cur)
	{
		aftercur = cur->next;
		free(cur);
		cur = aftercur;
	}
}

单链表的打印

打印单链表时, 我们只需要遍历单链表,并逐个打印每个结点的data成员即可。

需要注意的是,我们在遍历时,条件必须为cur,而不是cur->next。因为当cur->next为NULL时,cur是最后一个结点。此时,最后一个结点不进入循环,该结点的数据也不会被打印:

// 单链表打印
void SListPrint(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
}

单链表的头插与头删

头插

在顺序表中,想要从顺序表的前面插入数据是比较麻烦的,这需要将顺序表中的元素整体向后移动一个元素,而获得存放新与元素的空间;

但是在单链表中,头插的实现就比较简单,只需要将新创建的结点接入到单链表的前面即可。我们可以通过让新结点的next成员指向原plist,plist的值指向新结点的方式来实现:
详解单链表(内有精美图示哦)_第5张图片
需要注意的是:
在头插时,是需要改变结构体指针plist的值的,所以我们在传入该结构体指针时,需要传该结构体指针的地址,即二级指针。这样,才能实现将plist的值真正的改变:

当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。

void SListPushFront(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		newnode->next = *pplist;
		*pplist = newnode;
	}
}

头删

在顺序表中实现从前面删除也比较麻烦,需要将顺序表中的元素整体向前移动一个数据;

而单链表中只需要使plist指针指向链表中第一个结点的next成员即可。我们可以通过用一个结构体指针cur来记录plist的值,当plist指向下一个结点后,再释放cur指向的空间,即原第一个结点的空间:
详解单链表(内有精美图示哦)_第6张图片

同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。

当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。

void SListPopFront(SListNode** pplist)
{
	assert(*pplist);
	SListNode* cur = *pplist;
	*pplist = cur->next;
	free(cur);
	cur = NULL;
}

单链表的尾插与尾删

尾插

单链表需要在末尾插入数据时,首先需要找到单链表末尾的位置,然后将单链表最后一个结点的next成员改为新结点的地址即可。
详解单链表(内有精美图示哦)_第7张图片
在找最后一个元素时,我们可以通过cur指针向后移动,直到cur指向的结构体的next成员为NULL时,即cur指向的结点就是单链表的最后一个结点:
详解单链表(内有精美图示哦)_第8张图片
当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。
同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next)
		{
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

尾删

单链表删除末尾的数据时,同样的,我们需要找到单链表末尾的位置。

然后将单链表中倒数第二个结点的next成员改为NULL,然后释放cur(最后一个结点的指针)指向的空间。
我们可以通过创建一个beforecur变量来存储cur前一个结点的地址,这样,就可以实现当cur指向最后一个元素时,beforecur为倒数第二个元素:
详解单链表(内有精美图示哦)_第9张图片
当单链表中只有一个元素时,cur->next的值本身就是NULL。此时只需要释放cur指向的空间(第一个结点),然后将plist的值改为NULL即可。

同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。
当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);
	SListNode* cur = *pplist;
	SListNode* beforecur = NULL;
	while (cur->next)
	{
		beforecur = cur;
		cur = cur->next;
	}
	if (beforecur==NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		free(beforecur->next);
		beforecur->next = NULL;
	}
}

单链表的查找

之后,我们就会想到要删除单链表中指定的结点。
在删除指定的结点之前,我们首先需要实现一个算法,通过结点中的data成员找到这个节点的位置。并返回这个节点的指针。

遍历单链表,只需要将结构体指针cur依次后移即可。当cur->data的值为x时,返回cur。

// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

单链表在pos位置后插入/删除

在获取到了pos后,我们就会想要实现在pos位置进行插入或删除:

插入

在插入时,我们很容易想到将新节点newnode->next的值改为pos->next,然后将pos->next的值改为newnode。从而实现将pos位置插入数据:
详解单链表(内有精美图示哦)_第10张图片
显然,这样的算法只能实现在pos后增加结点,想要在pos位置增加结点,这样的条件显然是不足的。

所以我们就先来实现一下在pos后增加数据:

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

删除

在实现删除pos位置的结点时,我们会想到将pos->next的值改为pos->next->next位置的值,然后再释放pos后面的一块空间。
我们可以使用一个afterpos指针来暂存pos->next的值,以方便释放空间:
详解单链表(内有精美图示哦)_第11张图片
同样的,我们发现,这样删除只能释放pos后的一块空间。想要删除pos位置的结点,这样的条件显然是不足的。

但我们可以先实现一下这个函数:

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	SListNode* afterpos = pos->next;
	pos->next = afterpos->next;
	free(afterpos);
	afterpos = NULL;
}

单链表在pos位置插入/删除

要想在pos位置插入结点,或删除pos位置的结点,我们需要获取到pos结点前面的结点的指针,然后改变pos前面结点中的next成员,由此实现对pos位置数据的操作。

所以在传参的时候,我们需要将单链表第一个结点的地址传给函数,将第一个结点的地址向后遍历得到pos前一个结点的地址后,再进行操作。

我们可以定义一个beforepos指针:

详解单链表(内有精美图示哦)_第12张图片

插入

在获取到beforepos后,我们就可以重复上面的操作来实现在pos位置添加一个新结点:将新节点newnode->next的值改为beforepos->next,然后将beforepos->next的值改为newnode。从而实现在pos位置插入数据:
详解单链表(内有精美图示哦)_第13张图片
但是,在pos位置插入时是有特例的,即pos为单链表的第一个元素时,需要将plist的值改为plist,再将newnode->next的值改为pos即可:
详解单链表(内有精美图示哦)_第14张图片

// 单链表在pos位置插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	SListNode* beforepos = *pplist;
	if (beforepos == pos)
	{
		*pplist = newnode;
		newnode->next = pos;
	}
	else
	{
		while (beforepos->next)
		{
			if (beforepos->next == pos)
			{
				beforepos->next = newnode;
				newnode->next = pos;
				break;
			}
			beforepos = beforepos->next;
		}
	}
}

删除

当我们得到pos前的结点的地址后,就可以通过相同的方式实现删除pos位置的值:将beforepos->next的值改为beforepos->next位置的值,然后再释放pos后面的一块空间:
详解单链表(内有精美图示哦)_第15张图片

但是,有一种特例,即pos指向的是单链表的第一个元素时,我们只需要将plist的值改为pos->next;再将pos指向的空间释放即可:
详解单链表(内有精美图示哦)_第16张图片

// 单链表删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos)
{
	SListNode* beforepos = *pplist;
	if (beforepos == pos)
	{
		*pplist = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		while (beforepos->next)
		{
			if (beforepos->next == pos)
			{
				beforepos->next = pos->next;
				free(pos);
				pos = NULL;
				break;
			}
			beforepos = beforepos->next;
		}
	}
}

总结

到此,关于单链表的相关知识就介绍完毕了。当然,单链表是最简单的一种链表类型,在后面我们还会介绍一种比较复杂的链表,即带头双向循环链表。
当然,在介绍带头双向循环链表之前,我会先用一篇文章来讲解一些单链表的题目,欢迎大家持续关注哦

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

你可能感兴趣的:(数据结构初阶(C语言),链表,数据结构,开发语言,c语言,算法)