数据结构初阶第三篇——单链表(实现+动图演示)[建议收藏]

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构初阶第三篇——单链表(实现+动图演示)[建议收藏]相关的知识,希望对你有一定的参考价值。

上一篇博客我已经分享了顺序表的相关内容,这一篇博客,我又要来介绍一下单链表有关内容。
本篇博客代码链接:https://gitee.com/byte-binxin/data-structure/tree/master/SList_2


链表的概念

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

链表大概有这么三种,今天我主要给大家介绍第一种——单链表,下一篇博客我会给大家介绍双链表的有关内容。
值得注意的是:
1.从图中可知,链表的在逻辑是连续的,物理上不一定是连续的;
2.现实中节点是从堆上申请的。

链表的实现

链表的单个节点的定义

首先我们先对单个节点进行定义:

typedef int SLDataType;

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


就像这个图一样,一个空间用了存放数据,另一个空间用了存放下一个节点的地址。

链表的接口

我把链表主要的几个接口拿出来了,并且我们要实现这些接口:

//打印链表
void SListPrint(SListNode* phead);
//销毁链表
void SListDestory(SListNode** pphead);
//尾插
void SListPushBack(SListNode** pphead, SLDataType x);
//尾删
void SListPopBack(SListNode** pphead);
//头插
void SListPushFront(SListNode** pphead, SLDataType x);
//头删
void SListPopFront(SListNode** pphead);

//查找
SListNode* SListFind(SListNode* phead, SLDataType x);
//任意位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x);
//任意位置后删除
void SListEraseAfter(SListNode* pos);

这些接口实现起来都比较简单,但也值得我们主要,实现接口的同时要结合图来分析,否则程序一不小心就崩了。

单链表的尾插

首先,我们先来实现一个尾插的接口,尾插就是在链表的尾部插入一个节点。
在进行尾插的时候我们要考虑的几点:

  1. 此时链表是否为空,如果为空我们应该怎么做,不为空又应该怎么做,这两种情况我们要分开讨论;
  2. 这个接口接收的形参应该是一级指针还是二级指针;
  3. 如何申请节点,是在堆上还是栈上?

为了更好的理解尾插,我们先看一个动图展示:

看着动图我们再来思考上面的几个问题,
形参应该是二级指针。为什么呢?(涉及到头节点改变的都是,后面不重复说明

void SListPushBack(SListNode** pphead, SLDataType x);

当链表为空时,我们要申请一个节点,且这个节点会变成头节点。也就是原来链表指向的是NULL,我们要让这个指向发生改变,指向这个新开辟的节点,节点应该是在堆上开辟的,因为形参是实参的一份临时拷贝,所以传参来改变指针的指向只能通过二级指针来改变,一级指针是不行的。传一级指针不会head的指向。看下图:

代码实现如下:

void SListPushBack(SListNode** pphead, SLDataType x)
{
	SListNode* newNode = BuySListNode(x);
	//1.链表为空
	if (*pphead == NULL)
	{
		*pphead = newNode;
	}
	//2.链表不为空
	else
	{
		SListNode* cur = *pphead;
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		cur->next = newNode;
	}
}

插入五个数,代码测试结果:

这里我把申请节点封装成了一个函数,因为后面的头插和任意位置后插入也会用到,代码实现如下:

SListNode* BuySListNode(SLDataType x)
{
	SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
	newNode->data = x;
	newNode->next = NULL;
	return newNode;
}

单链表的尾删

尾删无非就是在链表的尾部删除一个节点,听起来很简单,但是有很多细节是我们要注意的,我们要分三种情况来进行讨论:

  1. 没有节点
  2. 只有一个节点
  3. 两个及两个以上的节点
    当然这里我们也是传二级指针。

先看动图演示:

代码分情况实现:

  1. 无节点
	assert(*pphead);//暴力解决
	if (*pphead == NULL)
	{
		return;//温柔解决
	}
  1. 1个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);//释放指针指向的空间
		*pphead = NULL;
	}
  1. 2个及2个以上的节点
    定义两个变量prev和cur,记住当前节点的位置和前一个节点的位置,以便释放节点空间。
	SListNode* prev = NULL;//前一个节点
	SListNode* cur = *pphead;//当前节点
	while (cur->next != NULL)
	{
		prev = cur;
		cur = cur->next;
	}
	free(cur);
	cur = NULL;
	prev->next = NULL;

完整代码实现如下:

void SListPopBack(SListNode** pphead)
{
	//分三种情况
	//1.没有节点
	//2.只有一个节点
	//3.两个及两个以上的节点
	if (*pphead == NULL)
	{
		return;
	}
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SListNode* prev = NULL;
		SListNode* cur = *pphead;
		while (cur->next != NULL)
		{
			prev = cur;
			cur = cur->next;
		}
		free(cur);
		cur = NULL;
		prev->next = NULL;
	}

}

单链表的头插

头插比较简单,无论是链表为空还是不为空都不用单独考虑,只要考虑到传二级指针就可以很好的实现了。

代码实现起来也比较简单,如下:

void SListPushFront(SListNode** pphead, SLDataType x)
{
	SListNode* newNode = BuySListNode(x);
	newNode->next = *pphead;
	*pphead = newNode;
}

单链表头删

头删就要分情况讨论:

  1. 链表为空
  2. 链表不为空

先看动图演示:


代码分情况实现:

  1. 链表为空
	assert(*pphead);//暴力解决
	if (*pphead == NULL)
	{
		return;//温柔解决
	}
  1. 链表不为空
	SListNode* firstNode = *pphead;
	*pphead = (*pphead)->next;
	free(firstNode);
	firstNode = NULL;

完整代码如下:

void SListPopFront(SListNode** pphead)
{
	if (*pphead == NULL)
	{
		return;
	}
	else
	{
		SListNode* firstNode = *pphead;
		*pphead = (*pphead)->next;
		free(firstNode);
		firstNode = NULL;
	}
}

单链表的打印

链表打印就是遍历一遍链表,只不过这里的遍历和数组有点不一样,链表的遍历是判断当前位置是不是为NULL,是就不打印了,不是就打印,通过·cur = cur->next·来移动当前位置。
代码实现如下:

void SListPrint(SListNode* phead)
{
	SListNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\\n");
}

单链表的查找

查找就是通过给出的节点地址来查找,并返回该节点的地址,找不到就返回NULL。因为这里没有涉及头节点的地址改变,所以不传二级指针
也是通过变量的方法,和单链表的打印有点类似,这里也不过多介绍,直接上代码:

SListNode* SListFind(SListNode* phead, SLDataType x)
{
	SListNode* cur = phead;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

单链表的任意位置后插入

我们选择在任意位置后插入,因为这样实现起来比较方便,不会涉及到要考虑链表记住前一个节点等等问题,我们简单实现就好了。
先看动图演示:

代码实现如下:

void SListInsertAfter(SListNode* pos, SLDataType x)
{
	assert(pos);
	SListNode* newNode = BuySListNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}

单链表的任意位置后删除

和上面一样,都是在任意位置后面删,因为实现起来比较简单,所以我就不多介绍。
先看动图演示:


代码实现如下:

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	SListNode* next = pos->next;
	if (next == NULL)
	{
		return;
	}
	else
	{
		SListNode* nextNext = next->next;
		free(next);
		next = NULL;
		pos->next = nextNext;
	}
}

单链表的销毁

链表的销毁也是依靠遍历来完成的,所以这里也直接放代码:

void SListDestory(SListNode** pphead)
{
	assert(pphead);
	SListNode* cur = *pphead;
	SListNode* next = NULL;
	while (cur)
	{
		next = cur->next;//记住下一个节点,以防找不到
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

总结

单链表在空间上不是连续的,但在物理上是连续的,可以做到按需所取,但不支持随机访问。链表和顺序表各有好处。下一篇博客我将给大家分享双链表的知识,并且归纳顺序表和链表的区别。欢迎大家评论区留言,点赞支持和指正~

以上是关于数据结构初阶第三篇——单链表(实现+动图演示)[建议收藏]的主要内容,如果未能解决你的问题,请参考以下文章

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

数据结构初阶第四篇——双链表(实现+图解)

C++初阶第三篇——初始C++(引用+内敛函数+auto关键字+范围for循环)

数据结构初阶第四节.链表详讲

C++初阶第十三篇—模板进阶(非类型模板参数+模板特化+模板的分离编译)