最详细八大链表结构和单链表基本操作教程


在这里插入图片描述

一.小生想说的话

本人在校大一小生一枚,希望大神们多多指点。上次我们学习了顺序表及其基本操作,现在我们来看看链表,如果大家觉得有用的话别忘了给小生一个关注,小生也会不断更新。加油,技术人!!!

二.认识链表

1.链表的基本概念

链表是N个数据元素的有限序列,它的长度可根据需要增长或缩短,同之前的顺序表一样,属于线性表中的一种。顺序表是顺序存储结构但链表是链式存储结构。
在这里插入图片描述

单链表用结点存储了数据以及下一个结点的地址,因此结点一般分为多个部分,即数据域与指针域,数据域存储有效数据,指针域存储下一个结点的地址。,同时单链表有只有一个指针域,双链表有两个指针域。

2.认识单链表与顺序表的区别与优缺点

这是两种不同的存储结构,我们先谈谈区别吧,顺序表是顺序存储结构,它的特点是逻辑关系上相邻的两个元素在物理位置上也相邻。但是链表不同,链式存储结构的特点是不需要逻辑上相邻的元素在物理位置上也相邻。因为链式存储结构可以通过结点中的指针域直接找到下一个结点的位置。 在这里插入图片描述
顺序表的优缺点:
1.优点:可以通过下标直接访问所需要的数据
2.缺点:不能按实际所需分配内存,只能使用malloc或者realloc函数进行扩容,容易实现频繁扩容,容易导致内存浪费与数据泄露等问题
单链表的优缺点:
1.优点:可以按照实际所需创建结点增减链表的长度,更大程度地使用内存。
2.缺点:进行尾部或者任意位置上插入或删除时时间复杂度和空间复杂度较大,每次都需要通过指针的移动找到所需要的位置,相对于顺序表查找而言效率较低。

3.认识八种链表的类型

在了解链表的类型之前,我们需要了解链表的几个特点
1.单向和双向
2.带头和不带头
3.循环和非循环

我们可以通过组合的方式,如:单向带头循环链表,双向不带头非循环链表……
小生画几个图让大家大体认识一下

~~单向带头循环链表

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

~~单向带头非循环链表

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

~~单向不带头循环链表

在这里插入图片描述

在这里插入图片描述

~~单向不带头非循环链表

在这里插入图片描述

在这里插入图片描述

~~双向带头循环链表

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

~~双向带头非循环链表

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

~~双向不带头循环链表

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

~~双向不带头非循环链表

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

二. 单链表的基本操作

我们以下所指的单链表是单向不带头非循环链表

1.基本操作的接口(基础)

单链表和顺序表类似,增删改查是对单链表基本的操作。我们先来浏览一下基础的接口!

在这里插入图片描述

2.单链表的结构定义

单链表的结点分为两部分,数据域和指针域。因此我们可以定义一个如下结构的结点

typedef int SLTDataType;
typedef struct SListNode
{
    
    
	SLTDataType data;
	struct SListNode* pNext;
}SLTNODE;

3.结点的创建

因为在后续的增加链表的长度,创建一个结点在某种程度上就是先创建一个该结构体指针类型变量再对进行初始化最后返回这个变量,我们可以通过代码直接分析。我们先看一下有头结点链表的创建

SLTNODE* CreateNode(SLTDataType val)
{
    
    
	SLTNODE* pNew = (SLTNODE*)malloc(sizeof(SLTNODE));
	pNew->data = val;
	pNew->pNext = NULL;
	return pNew;
}

4.分辨传送一级指针与二级指针

如果要改变链表的头指针就传二级指针,改变头指针不能传一级指针因为传送的过程就是拷贝的过程,相当于将头指针复制了一份,形参的改变不会影响实参,因此要改变链表的头指针需要传送二级指针。

5.单链表的插入

~~单链表的头插

因为我们前面强调过,我们进行操作的单链表是不带头结点的,因此头插就非常方便,至于带头结点的单链表如何操作,也挺简单的,相信大神们都能自己写出来吧

//因为要改变头指针所以我们要传送头指针的地址,即二级结构体指针变量
 void SListPushFront(SLTNODE** ppHead, SLTDataType val)
{
    
    
	
	SLTNODE* pNew  = CreateNode(val);
	pNew->pNext = *ppHead;
	*ppHead = pNew;
}

~~单链表的尾插

尾插的时候要考虑单链表是否为空的情况,因为如果为空,我们要将头指针指向新的结点。

void SListPushBack(SLTNODE** ppHead, SLTDataType val)
{
    
    
	SLTNODE* pNew = CreateNode(val);
	//判断链表是否为空,若为空则将头指针指向新结点
	if (*ppHead == NULL)
	{
    
    
		*ppHead = pNew;

	}
	else
	{
    
    
		SLTNODE* pTail = *ppHead;
		//通过循环让指针找到尾部
		while (pTail->pNext != NULL)
		{
    
    
			pTail = pTail->pNext;
		}
		pTail->pNext = pNew;
	}
	
}

不知道大神们有没有注意到我们循环的条件是pTail->pNext != NULL 可不可以改为pTail != NULL呢?乍一看好像没什么问题,但是真的没有问题吗?我们要好好思考一下~~

我们先看正确的循环条件pTail->pNext != NULL

为了方便我们在图里用pHead代替*ppHead在这里插入图片描述此时pTail->pNext != NULL 成立,pTail指针后移在这里插入图片描述此时pTail->pNext != NULL 成立,pTail指针后移
在这里插入图片描述
注意,此时后面没有结点了,则此时pTail所指向的结点里面的指针域存放的是空指针,即pTail->pNext为空,pTail刚好指向最后一个结点。我们再来看看循环条件为pTail != NULL的情况
在这里插入图片描述
此时pTail != NULL 成立,pTail指针后移
在这里插入图片描述
此时pTail != NULL 依然成立,pTail指针后移
在这里插入图片描述
此时pTail != NULL 依然成立,pTail指针后移
在这里插入图片描述
从上面我们可以发现,pTail != NULL这个条件执行时,当pTail指向尾结点时也不会停止,因此该循环条件是错误的

~~单链表的指定位置插入

~~在pos位置之前插入

这里我们要分两种情况,pos是第一个结点和pos不是第一个结点,如果pos为1就相当于头插,如果pos不为1就用两个指针通过循环找到pos和pos的前一个结点。
在这里插入图片描述
在这里插入图片描述

void SListInsert(SLTNODE** ppHead, SLTNODE* pos, SLTDataType val)
{
    
    
	//1.pos是第一个结点,在pos之前插入相当于头插
	if (*ppHead == pos)
	{
    
    
		SListPushFront(ppHead, val);
	}
	//2.pos不是第一个结点
	SLTNODE* pPrev = NULL;
	SLTNODE* pMove = *ppHead;
	while (pMove != pos)
	{
    
    
		pPrev = pMove;
		pMove = pMove->pNext;
	}
	SLTNODE* pNew = CreateNode(val);
	pPrev->pNext = pNew;
	pNew->pNext = pos;
}

这里我们用了两个指针,我们可以通过一个指针直接实现吗?我们试一下

void SListInsert(SLTNODE** ppHead, SLTNODE* pos, SLTDataType val)
{
    
    
	if (*ppHead == pos)
	{
    
    
		SListPushFront(ppHead, val);
	}
	 SLTNODE* pPrev = *ppHead;
	 while(pPrev->pNext != pos)
	 {
    
    
		  pPrev = pPrev->pNext;
	 }
	 SLTNODE* pNew = CreateNode(SLTDataType val);
	 pPrev->pNext = pNew;
	 pNew->pNext = pos;
	
	SLTNODE* pNew = CreateNode(val);
	pPrev->pNext = pNew;
	pNew->pNext = pos;
}

显而易见是可以的,因为pos指针已经确定了,因此不必像之前尾插的时候找尾,只需要找到pos前一个结点就行。
在这里插入图片描述
在这里插入图片描述

~~在pos位置之后插入

单链表可以通过前一个结点找到下一个结点,不能通过后面的结点找到前面的结点。因此,在pos位置后面插入就没必要向之前在pos位置之前插入时需要通过指针的循环移动找节点了,因为通过pos可以直接找到下一个结点

 void SListInsertAfter(SLTNODE * pos, SLTDataType val)
{
    
    
	assert(pos);
	SLTNODE* pNew = CreateNode(val);
	pNew->pNext = pos->pNext;
	pos->pNext = pNew;
}

6.单链表的删除

删除和插入在一定程度上是相似的,可以类比一下~~~

~~单链表的头删

释放原来的第一个结点,头指针移动到下一个结点在这里插入图片描述> 在这里插入图片描述

void SListPopFront(SLTNODE** ppHead)
{
    
    
	assert(*pHead);
	SLTNODE* next =  (*ppHead)->pNext;
	free(*ppHead);
	*ppHead = next;

}

~~单链表的尾删

尾部删除时要考虑三种情况链表是否为空,链表只有一个结点和链表有多个结点

void SListPopBack(SLTNODE** ppHead)
{
    
    
	//1.链表为空
	assert(*ppHead);
	//2.只有一个结点
	if ((*ppHead)->pNext == NULL)
	{
    
    
		free(*ppHead);
		*ppHead = NULL;
	}
	//3.有两个及以上的结点
	SLTNODE* pPrev = NULL;
	SLTNODE* pTail = *ppHead;
	while (pTail->pNext != NULL)
	{
    
    
		pPrev = pTail;
		pTail = pTail->pNext;
	}
	free(pTail);
	pPrev->pNext = NULL;
}

~~单链表的指定位置删除

当pos等于头指针时,相当于链表的头删,我们可以直接调用之前写过的头删函数SListPopFront()

void SListErase(SLTNODE** ppHead,SLTNODE* pos)
{
    
    
	if (pos == *ppHead)
	{
    
    
		SListPopFront(*ppHead);
	}
	else
	{
    
    
		SLTNODE* pPrev = *ppHead;
		while (pPrev->pNext != pos)
		{
    
    
			pPrev = pPrev->pNext;
		}
		pPrev->pNext = pos->pNext;
		free(pPrev);
	}
}

7.单链表的查找

通过指针不断移动找到数据域里面存储对应数据的结点并返回指针

SLTNODE* SListFind(SLTNODE* pHead,SLTDataType val)
{
    
    
	SLTNODE* pMove = pHead;
	while (pMove != NULL) //while(pMove)
	{
    
    
		if (pMove->data == val)
		{
    
    
			return pMove;
		}
	}
	return NULL;
}

8.单链表的销毁

销毁操作应该是最简单了啦,小生就不罗嗦了,相信大神们一看就懂

 void SListDestroy(SLTNODE** pphead)
{
    
    
	SLTNODE* pMove = *pphead;
	while (pMove !=NULL)
	{
    
    
		SLTNODE* next = pMove->pNext;
		free(pMove);
		pMove = next;
	}

	*pphead = NULL;
}

四.结语

非常感谢大神们能看到这里,小生在此谢谢大家了,小生带着大家粗略认识了八种链表的结构和单链表的基础操作,码字不易,原创不易,请大家多多支持呀!!!下一篇我们就进入双链表和循环链表了,加油技术人(别忘了点赞加关注哦)~~~
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_59955115/article/details/123588720
今日推荐