您现在的位置是:首页 >技术杂谈 >【数据结构】一篇带你彻底玩转 链表网站首页技术杂谈

【数据结构】一篇带你彻底玩转 链表

虾米Life 2023-06-26 12:00:02
简介【数据结构】一篇带你彻底玩转 链表

链表的概念及结构

链表是一种物理存储结构上非连续、非顺序的存储结构,数是据元素的逻辑顺序是通过链表中的指针链接次序实现的 。C语言结构体指针在这里得到了充分的利用,可以在节点中定义多种数据类型, 并可以根据需要进行增删查改功能.

对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。想象一下,最后一个结点,它的指针指向哪里?

最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为“空” 用NULL表示

  • 单链表的存储结构
struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
};
  • 链表存储结构图
    在这里插入图片描述
    注意:
  • 图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
  • 现实中的结点一般都是从malloc堆上申请出来的
  • 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
    构的子结构
    在这里插入图片描述
  1. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了
    在这里插入图片描述
  • 该章节我们讲解的是无头单向循环链表

链表接口的实现

无头+单向+非循环链表增删查改实现

链表打印

  • 链表与顺序表不同,顺序表是通过下标来访问每个元素。而链表是通过结构体存储的指针指向下一个节点来遍历整个数据的.
void SListPrint(SListNode* plist)
{
	assert(plist); //断言 判断是否为空指针
	SListNode* tmp = plist; 
	while (tmp != NULL)  //遍历整个链表,直到tmp指针指向空
	{
		printf("%d-> ", tmp->data); //打印节点数据
		tmp = tmp->next;	//将指针指向下一个结点
	}
	printf("NULL
");
}

链表申请节点

  • 链表添加一个节点数据时候,每次都要写一段代码,这样做是不是太繁琐了,我们可以封装一个函数来解决问题,每次添加一个节点时,将结点存放的数据置为需要存放的值,在将结构体存放节点的地址置为NULL, 需要增加节点时调用一下改函数即可。
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode)); //申请一个结构体节点
	
	//判断是否申请成功
	if (newnode == NULL)
		perror("malloc:
");
	
	newnode->data = x;
	newnode->next = NULL;
	//申请成功返回节点
	return newnode;
}

链表尾插

  • 在单链表进行尾插时,我们需要遍历整个链表直到找到最后一个节点才可以进行尾插,但如果此时链表为空时,我们直接插入节点数据即可。

错误代码示例:
相信许多人会写出这样的代码在这里插入图片描述 错误原因:

tail最后找到NULL了,tail和newnode 都是一个临时变量,把newnode给了tail,tail存放的是newnode里面的内容,tail出了作用域就不存在了。把newnode给了tail 并不会链接上链表的节点。最后还会存在内存泄漏. 所有我们这里要使用结构体指针来链接节点,正确写法应该让上一节节点找下一个节点的地址,让tmp->next (next这些节点都是在堆上的,并不会销毁) 找尾 ,将结构体指针指向的尾结点(NULL)指向新结点

正确代码示例

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	//申请一个节点
	SListNode* newnode = BuySListNode(x);
	
	//当链表还没有节点时为空时,我们直接插入数据即可.
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		//进行找尾进行尾插
		//找到指针指向的尾结点(NULL)指向新结点
		SListNode* tmp = *pplist;
		while (tmp->next != NULL)  
		{
			tmp = tmp->next;
		}
		tmp->next = newnode;//将尾结点中存放的指针置为插入结点的地址即可链接上
	}
}

链表头插

  • 头插逻辑相对简单.只需要申请一个节点,让节点的指针指向头指针,在把头指针指向申请节点的位置
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	//申请一个节点
	SListNode* newnode = BuySListNode(x);
	
	newnode->next = *pplist;
	*pplist = newnode;
}

链表尾删

  • 尾删的细节较多一点
    1: 链表为空时,无须删除
    2: 链表只有一个元素时,做特需处理
    3: 正常处理

这里提供两种方法给大家参考

  • 方法一: 前后指针
    使用一个指向为空的指针 prev和一个指向头节点的指针cur,当cur->next指向的不是NULL时我们让prev 指向cur节点,而cur指向cur的下一个节点,循环直到cur->next指向null时,prev->next指向的就是我们要删除的最后一个节点.
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);  //断言 判断是否为空指针
	
	//当只有一个节点时
	if ((*pplist)->next == NULL)
	{
	//只有一个结点,直接释放该结点,然后将结点置为NULL
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* cur = *pplist; 
		SListNode* prev = NULL;
		while (cur->next != NULL)
		{
			prev = cur;	
			cur = cur->next; //循环遍历
		}
		free(prev->next); //释放尾结点
		prev->next = NULL;
	}
}
  • 方法二: 知需判断cur->next->next是否为空,如果为空释放cur->next。当链表只有两个节点时循环不进去,直接释放cur->next.
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);//断言 判断是否为空指针

	//只有一个结点,直接释放该结点,然后将结点置为NULL
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next->next != NULL)
		{
			cur = cur->next;//循环遍历
		}
		free(cur->next); //释放尾结点
		cur->next = NULL;
	}
}

链表头删

  • 链表头删逻辑也简单,只需要注意一下链表是否为空。然后用一个临时变量保存头指针的->next,然后在把头指针销毁,把头指针给回临时变量即可.
// 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(*pplist);  //断言 判断是否为空指针
	
	SListNode* tmp = (*pplist)->next;//保存头指针的下一个节点
	free(*pplist); //销毁释放头指针
	*pplist = tmp; 头指针指向释放前的下一个节点
}


链表查找

  • 获得链表某个节点的数据思路也较简单
    1: 定义一个cur指针指向头节点,不断指向下一个节点
    2: 如查找成功,返回节点cur的数据
    3: 如cur指向Null已经遍历完了,则说明查找的内容不存在,返回空指针。
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);//断言 判断是否为空指针
	SListNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

链表在指定位置之后插入数据

  • 先把新节点的next 指向pos->next, 然后再把pos->next 更新成新节点.
void SLInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);//断言 判断是否为空指针

	SListNode* newnode = BuySListNode(x);//申请一个节点
	newnode->next = pos->next; //新节点指向pos位置的下一个节点
	pos->next = newnode; //pos下一个位置插入新节点
}

链表删除指定位置之后的数据

  • 整体逻辑就是保存将要删除位置下一个节点的位置,把pos->next后的位置在链接上要删除位置的下一个节点。 如果pos下一个节点为空,无需删除直接返回即可.
void SListEraseAfter(SListNode* pos)
{
	assert(pos);//断言 判断是否为空指针
	
	if (pos->next == NULL) //如果pos下一个节点为空,无须删除
		return;
	SListNode* cur = pos->next->next;//保存要删除位置的下一个节点
	free(pos->next); //释放pos之后的数据
	pos->next = cur; //链接上要删除位置的下一个节点
}

链表在指定位置之前插入数据

  • 算法思路:
    1: 当指定位置为第一个元素时,这时我们只需调用一下头插函数即可.
    2: 遍历链表,找到pos上一个节点的数据.
    3: 找到上一个节点数据,把新节点的next指向指定位置,在把找到的节点指向新开辟的节点,这样就可以链接上了。
void SLInsertfront(SListNode** pplist, SListNode* pos, SLTDateType x)
{
	assert(pos);//断言 判断是否为空指针
	if (*pplist == pos) //当pos位置是第一个节点数据时,那就是头插了
	{
		SListPushFront(pplist, x);//头插
	}
	else
	{
		SListNode* tmp = *pplist;
		while (tmp->next != pos)
		{
			tmp = tmp->next;//循环遍历
		}
		SListNode* newnode = BuySListNode(x);
		newnode->next = pos; //把新节点的next指向指定位置
		tmp->next = newnode; //找到的节点指向新开辟的节点
	}
}

链表删除指定位置之前的数据

  • 整体思路:
  • 1: 当指定位置pos是第一个节点时,无需删除,直接返回即可。
  • 2: 当指定位置pos是第二个节点数据时,只需要进行头删即可。
  • 3: 遍历数组,找到pos 之前要删除节点数据的上一个节点。把该节点的next(就是pos上一个节点的数据)直接销毁释放掉,再把找到的节点next重新链接上pos.
void SLErasefront(SListNode** pplist, SListNode* pos)
{
	assert(pos); //断言 判断是否为空指针

	if (pos == *pplist) //当指定是第一个节点时,无需再删除,直接返回.
	{
		printf("pos位置前为空
");
		return;
	}
	else if ((*pplist)->next == pos)//当指定位置pos是第二个节点数据时,只需要进行头删即可
	{
		SListPopFront(pplist);//头删
	}
	else
	{
		SListNode* tmp = *pplist;
		while (tmp->next->next != pos)
		{
			tmp = tmp->next; //遍历循环
		}
		free(tmp->next);//释放pos之前的节点数据
		tmp->next = pos; //链接上pos
	}
}

链表删除指定位置的数据

  • 思路:
    1: 当指定位置pos是第一节点数据时,直接使用头删即可。
    2: 查找指定位置pos上一个节点位置,再把该位置的next链接上pos的next位置.
    3: 释放指定位置的数据。
void SLErase(SListNode** pplist, SListNode* pos)
{
	assert(pos);//断言 判断是否为空指针
	
	if (pos == *pplist)//当pos是第一节点数据时,直接使用头删即可
	{
		SListPopFront(pplist);//头删
	}
	
	else
	{
		//
		SListNode* tmp = *pplist;
		while (tmp->next != pos)//找pos上一个节点
		{
			tmp = tmp->next;
		}
		tmp->next = pos->next;//将需要删除的结点的上一个结点的next指向需要删除的下一个结点
		free(pos);//释放指定位置的数据
		pos = NULL;
	}
}


链表的销毁

当我们不打算使用这个单链表时,我们需要把它销毁,其实也就是在内存中将它释放掉,以便于留出空间给其他程序或软件使用。

  • 思路:保存当前节点的下一个结点的地址,释放当前结点,再将指针指向刚刚保留的结点,如此循环直到为空。链表销毁成功.
// 单链表的销毁
void SListDestroy(SListNode* plist)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur != NULL)
	{
		SListNode* next = cur->next; //保存下一个节点
		free(cur); //释放当前指定
		cur = next; //指向下一个节点
	}
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。