【数据结构】单链表(超全)

这篇具有很好参考价值的文章主要介绍了【数据结构】单链表(超全)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【数据结构】单链表(超全)


前言:
 上一次我们分享了线性表中的一种结构顺序表,它存在着一些其缺点,比如:在中间位置或者头部进行元素插入或者删除的时候时间复杂度是 O ( N ) O(N) O(N)效率比较低,并且顺序表在扩容的时候也存在时间和空间上的消耗,由于我们每次都是按照二倍来扩的,那就很有可能会出现扩大了用不完导致空间浪费的现象。这些问题该如何解决呢?那就需要用到今天分享给大家的另一种线性结构 链表

一、什么是链表?

1.1 定义

 链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
 简单来理解就是,链表中的每一个数据元素都是独立存储的,当需要存储一个数据元素的时候就去向内存空间申请一块内存用来存储当前数据,每一个数据元素又通过地址串联在一起,因此对于链表这个结构来说,不仅需要存储当前的数据元素,还需要存储下一个元素的地址,这样才能把数据元素串联在一起,通常我们把待存储的数据元素和下一个数据的地址合起来叫做链表中的一个节点,这个节点由两部分数据组成,因此可以定义一个结构体来创建节点。
图示:
【数据结构】单链表(超全)

1.2 链表的分类

 链表会根据是否带头结点、是否是双向链表、是否是循环链表进行分类组合。因此链表一共有八种具体的实现形式。
 其中头节点是在链表的第一个节点之前附设一个类型相同的节点,我们把这个节点就称之为头节点,当然头节点和普通的节点在形式上并没有什么不同,所以头节点也分数据区域指针区域,头结点的数据区域建议什么东西也不存储,有些地方会在头结点的顺序区域存储链表的长度,其实这是欠妥的,因为链表的长度(也就是节点个数)一定是一个int型的整型变量,只有当链表存储的全是整型数据的时候,节点的数据域是整型,此时在头结点的数据域存储链表长度是可以的,但是当链表存储的是字符型或者其他用户自定义类型的时候,那节点的数据域就不是整型了,此时再在头结点的数据域存储链表长度显然是不行的。所以,为了使我们写出来的链表更加具有普适性,这里不建议大家在头结点的数据域存储链表长度。头结点的指针域存储的则是链表中第一个节点的地址。关于链表的优点这里先留一个悬念,相信随着学习的深入小伙伴们自然就能体会到头结点的优势。
【数据结构】单链表(超全)

 这里大家需要注意区分头节点头指针,他俩是两回事。

  • 头节点是一个节点,本质上是一个结构体变量,区分数据域和指针域
  • 头指针是一个指针,本质上是一个结构体类型的指针变量,不区分数据域和指针域,它仅存储链表中第一个节点的地址。

 双向链表,即一个节点中有两个指针域,一个存放当前节点前一个节点的地址,另一个存放当前节点后一个节点的地址。随着学习的深入我们就会发现双向链表的优势,这里就不过多赘述。
【数据结构】单链表(超全)

 循环链表,即链表的最后一个节点的指针域存的是第一个节点的地址,这样一来整个链表就形成了一个环达到了循环的效果。
【数据结构】单链表(超全)

 至此,链表的分类就给大家介绍完了,虽然根据链表的不同分类标准可以组合出八种不同结构的链表,但我们只要掌握了无头单向非循环带头双向循环这两种结构,剩下的六种便能轻松上手。所以下面我将详细的介绍一下这两种结构。

二、无头单向非循环链表

【数据结构】单链表(超全)

2.1 结构

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;//数据域
	struct SListNode* next;//指针域
}SLTNode

 无头单向非循环链表的结构就只需要数据域一个指针域。数据域的类型是待存储数据的类型,这里为了使链表更具有普适性,这里使用了typedef对类型重命名。指针域要存放的是下一个节点的地址,因此它的类型是一个结构体类型的指针,需要注意:结构体的类型是struct SListNode一定要写全,不能漏写,也不能用SLTNode来声明指针,因为在声明指针的时候还没有对结构体类型进行重命名。为了方便起见,对结构体类型进行重命名。

链表与顺序表在结构上的区别:
【数据结构】单链表(超全)
 从图中可以看出,链表的每一个数据是直接存储在一个结构体变量中,多个结构体变量共同组成一个链表,而顺序表则是在一个结构体变量的基础上,通过它的成员arr指向动态申请的空间,顺序表中的数据并没有直接存储在结构体变量中,而是存储在动态申请的开空间里,一个顺序表只对应一个结构体变量。结构上的差异导致pList == NULLps == NULL所表达的含义是不同的,pList == NULL表示当前链表是一个空链表,当然空链表也是一个链表,只不过里面没有数据罢了,而ps == NULL则说明这个顺序表根本就不存在,这里注意:ps == NULL不表示一个空顺序表,ps->size == 0才表示一个空顺序表(前提是ps非空)。

2.2 如何遍历链表数据

 在知道了什么是链表以及链表的结构之后,我们需要把链表利用起来,即完成对链表的增、删、查、改等工作。在进行这些工作之前,我们首先得知道如何去遍历链表访问链表中的每一个数据,然后才能去完成前面的工作。遍历过程如下代码所示:

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

 首先形参接收到的是链表中第一个节点的地址,要遍历链表当然要从第一个结点开始嘛,这一点相信大家很容易理解,接下来就是遍历了,遍历是通过一个指针去维护的,可以这么说对链表的所有操作都是用一个指针去维护的,通过改变指针的指向以达到访问不同节点的目的,因为链表是由多个节点共同组成的,说白了也就是多个结构体变量共同组成的,如果是值传递,那这个链表有多少个节点,我们就得在函数中再创建多少个节点来接收,这在造成空间浪费的同时也大大增加了我们处理问题的复杂度。在明确了用指针去遍历链表之后,接下来就该让这个指针动起来。cur = cur->next;这条语句就实现了让指针动起来,把cur指向的节点的指针域中存的地址赋值给cur自己,而cur指向的节点的指针域存放的就是下一个节点的地址。直到cur == NULL的时候说明已经遍历完了整个链表。需要注意遍历结束的条件不能是:cur->next != NULL,因为cur->next == NULL说明cur指向最后一个节点,并没有遍历结束。下面给出遍历过程的物理示意图和逻辑示意图。
物理结构示意图:
【数据结构】单链表(超全)
逻辑结构示意图:
【数据结构】单链表(超全)

2.3 尾插

 尾插的第一步当然是先创建一个节点(也就是结构体类型的变量)来存储数据,注意尾插我们是通过函数来实现的,因此在创建新节点的时候不能用SLTNode newnode来创建节点,因为这样创建的节点是一个局部变量,函数结束局部变量就销毁了,通常情况下是通过malloc去堆上申请一块空间来存储数据。节点创建好后,就要想办法把此节点链接到原链表的最后,因此接下来要先找到原链表的最后一个节点,然后让最后一个节点的指针域存放新创建节点的地址,这样就实现了链接,下面还是通过逻辑结构示意图和物理结构示意图演示一下尾插的过程:
逻辑结构示意图:
【数据结构】单链表(超全)
物理结构示意图:
【数据结构】单链表(超全)
 尾插还有一种情况需要注意,那就是当链表为空的时候。上面提到过一个链表为空,意味着链表只有一个节点,并且该节点的地址是0x00000000。首先第一步还是创建节点把要尾插的数据先存储起来,然后呢?把这个节点链接到这个地址是0x00000000的节点后面嘛?答案是不行的因为0x00000000所在的地址空间是不允许我们随意访问的,它属于操作系统严格管控的区域,这就意味着我们无法去访问到这块空间的指针域然后把新节点的地址存进去。
 对于空链表尾插的正确做法是,直接将新创建的节点当作头节点。这就意味着:需要把头指针中存放的地址修改成新创建节点的地址!,要记得尾插的所有操作都是封装成函数来实现的,对于函数来说形参的改变不会影响实参,而这里需要修改一个指针变量里面存放的地址,并且希望在函数里面修改之后,在函数外面依然生效,因此这里我们需要进行址传递,也就是传递头指针的地址地址的地址那形参自然就需要用一个二级指针来接收,这里记作pphead。注意:这个二级指针不能为空,因为他存的是指向头节点指针的地址,如果为空那就说明没有这个指向头结点的指针,那就说明链表不存在,要区分链表不存在空链表各自是如何表示的。

  • 空链表:头指针为空,也就是pList == NULL表示的是一个空链表,有些操作时允许空链表的情况的,比如说遍历、尾插、头插数据,而有些情况则不允许出现空链表的情况,比如说头删、尾删数据。因此我们需要根据具体的情况去做检查。
  • 链表不存在pphead ==NULL则是说明链表不存在,对链表的操作,链表不存在当然是不被允许的,所以只要使用了pphead就要对他进行检查。
    代码实现:
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
	assert(pphead);//pphead不能为空,pphead为空说明里面没有存指向头节点指针的地址,那就说明没有链表
	SLTNode* newnode = BuySLTNode(x);//由于在头插和随机插入的过程中也会涉及到节点的创建,所以这里把节点的创建单独封装了一个函数

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != NULL)//假如链表为空这里就非法访问了,因此要先判断
		{
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

2.4 创建新节点

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

2.5 头插

 头插显然是要改变头指针存放的地址,因此形参也需要传递二级指针。头插无需单独考虑空链表的情况
代码实现:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

2.6 尾删

 尾删先要遍历一遍链表找到最后一个节点将其释放掉,还要找到倒数第二个节点将它的指针域中存的地址改为NULL。所以定义两个指针让他们同时去遍历链表,一个找尾,另一个找倒数第二个节点。需要注意的是空链表和只有一个节点的链表的情况,空链表无法进行尾删,而只有一个节点的链表在尾删后会变成一个空链表,这意味着要改变头指针里面存放的地址,所以尾删形参也要传递二级指针。
代码实现:

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);//暴力检查是否为空链表,空链表不能删数据
	//检查链表是否为空
	/*if (*pphead == NULL)//温柔的进行检查
	{
		return;
	}*/
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//有多个节点
	else
	{
		//找尾
		SLTNode* prev = *pphead;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}

		free(tail);
		tail = NULL;

		prev->next = NULL;//假如只有一个节点这里就会非法访问
	}	
}

2.7 头删

 头删很明显需要改变头指针中存放的地址,所以形参仍然需要传递二级指针。头删只需要注意链表是否为空,空链表无法进行删除。此外在进行头删的时候记得将原来的头节点释放掉,因此在改变头节点之前需要先保留原来头结点的地址,否则在更换完新的头节点后就找不到原来的头节点了。
代码展示:

void SLTPopFront(SLTNode** pphead)
{
	if (*pphead == NULL)//这里也可以直接用assert来断言
	{
		return;
	}
	SLTNode* tail = *pphead;
	*pphead = (*pphead)->next;//假如链表为空,这里就会发生越界,因此要判断链表是否为空
	free(tail);
	tail = NULL;
}

2.8 单链表查找

 其实就是遍历一遍链表,但是只能返回第一次出现的地址。查找可以当修改来使用,我们查找到节点的地址后就可以通过地址去修改数据域中存储的数据。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* ptr = phead;
	while (ptr != NULL)
	{
		if (ptr->data == x)
		{
			return ptr;
		}
		else
		{
			ptr = ptr->next;
		}
	}
	return NULL;
}

2.9 在pos位置之前插入

 和尾插类似,但此时只需要遍历链表找到pos位置的前一个节点即可,同样需要注意pos是头结点的情况,此时就成头插了,需要改变头指针中存的地址,因此函数的形参需要传二级指针。

//在pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

 上面这种在pos位置前面插入的方法,需要知道头节点的地址

2.10 删除pos位置数据

 pos可能是头结点的地址,因此形参要用二级指针。

//pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*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;//其实没用,形参的改变不改变实参
	}
}

2.11 在pos位置的后面插入

 这里需要特别注意地址的赋值顺序。有以下两种正确的赋值顺序供大家参考:

  • 先让newnode的指针域存储pos后一个节点的地址,再让pos的指针域存newnode的地址
  • 借助中间变量先把pos后面节点的地址保存起来,再让pos的指针域存newnode的地址,最后再让newnode的指针域存第一步中间变量中保存的地址。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* tmp = pos->next;
	pos->next = newnode;
	newnode->next = tmp;
}

2.12 删除pos位置后面的数据

 注意不能写成后面这样:pos->next = pos->next->next。这样写虽然把pos位置后面的节点从链表中剔除出去了,但并没有真正意义上的实现删除,因为每一个节点都是通过malloc在堆上申请的,不使用的时候要主动的去释放掉,也就是free掉,把这块空间归还给操作系统,否则会导致内存泄漏。而上面那样写,就会导致pos后面的节点丢失,无法进行释放,正确的做法是在执行这条语句之前把pos后边节点的地址先保存起来。

void SLTEraseAfter(SLTNode* pos)
//只能删除pos位置后面的节点,不能删除pos节点
//因为pos节点的前一个节点无从得知
{
	assert(pos);
	assert(pos->next);
	SLTNode* tmp = pos->next->next;//这里先保存了pos后面的后面的节点的地址,也是可以的
	free(pos->next);
	pos->next = tmp;
}

 今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!
【数据结构】单链表(超全)文章来源地址https://www.toymoban.com/news/detail-515282.html

到了这里,关于【数据结构】单链表(超全)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C语言数据结构(0)——前言

    欢迎来到博主的新专栏——C语言与数据结构 博主id:代码小豪 在前两个专栏当中,博主已经大致的讲过了C语言中的大部分使用方法。大家都知道,学习英语时,首先掌握的是单词,随后学习语法,如此才能融会贯通的学习英语。如果学英文只会单词,那么阅读虽然不成问题

    2024年01月17日
    浏览(29)
  • 数据结构-双向链表(c++)超全超详细

    单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点(插入,删除操作时),只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。 提示:以下是本篇文章正文内容,下面案

    2023年04月08日
    浏览(33)
  • 【数据结构】数据结构小试牛刀之单链表

    不讲虚的啦,直接肝! 单链表所要实现的功能罗列如下: 初始化工作我们先初始化一个节点类型,类型中包括了数据域和指针域,数据与中保存着该节点要保存的数据,指针域则保存着链表下一个节点的地址: 然后我们在创建一个函数,用于创建一个新的节点,因为后面我

    2023年04月24日
    浏览(50)
  • 【数据结构】单链表(一)

    作者:日出等日落 专栏:数据结构 想变成仲夏夜的一只萤火虫,只要抓住你的注意力,就已经满足了。 目录 前言:  单链表的结构 :  逻辑结构: 物理结构: BuySLTNode(动态申请一个结点):   CreateSList(//循环创建结点): SLTPrint(单链表打印):  单链表的功能:  SL

    2024年02月01日
    浏览(30)
  • 数据结构入门——单链表

    目录 前言 链表的定义 链表的创建 头插法创建链表 尾插法创建链表 链表的删除 在头部删除元素 在尾部删除元素 查找固定元素 在链表中插入元素 在pos位置前插入 在pos位置后插入 删除链表结点 删除pos位置的结点 删除pos后的结点 链表的销毁 写在最后 前面我们学习了顺序表

    2024年02月20日
    浏览(29)
  • 数据结构·单链表

            不可否认的是,前几节我们讲解的顺序表存在一下几点问题:         1. 中间、头部的插入和删除,需要移动一整串数据,时间复杂度O(N)         2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗         3. 增容一般是2倍的增长,这势

    2024年01月25日
    浏览(64)
  • 数据结构-单链表

    概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。  从以上图片可以看出: 1.链式结构在逻辑上是连续的,但在物理上不一定是连续的。 2.现实中的节点一般是在堆上申请出来的。 3.从堆上申请的空间,

    2024年02月05日
    浏览(30)
  • 数据结构:单链表

    单链表的全称为\\\"不带头单向不循环链表\\\" 注意:         下文提到的头节点与带头链表时两个概念,文中的头节点指的是第一个有效节点,二带头节点中的头指的是第一个无效节点 链表和顺序表一样,都是线性表,逻辑结构上是线性的,但不同的是,链表在物理结构上不是线性的

    2024年01月21日
    浏览(30)
  • 数据结构--单链表

    目录 1.单链表的定义: 单链表基本操作的实现: 2.单链表的初始化(构造带头结点的空表) 2.将头结点的指针域置空 3.链表是否为空: 4.单链表的销毁: 5.单链表的清空: 6.求单链表的表长: 7.   取值  取单链表第i个元素: 8按值查找 根据指定数据查找指定数据所在位置序

    2024年02月15日
    浏览(26)
  • 数据结构单链表

    单链表 1 链表的概念及结构 概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链 接次序实现的 。 在我们开始讲链表之前,我们是写了顺序表,顺序表就是类似一个数组的东西,它的存放是连续的,优点有很多,比如支持

    2024年02月12日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包