【数据结构】单链表及双向链表的解析与实现

前言:单链表与双向链表都是线性表的一种,线性表在逻辑上都是连续的但在空间上不一定是连续的,前面我们说过的顺序表在空间上就是连续的,但今天讲的单链表与双向链表在空间上都是不连续的,双向链表更像是单链表的改版,所以当理解了单链表时再去学习双向链表就会很轻松,所以本文将围绕单链表与双向链表展开,并将其实现

1.链表的定义

在前面我们学过的顺序表中,我们虽然在实现的动态顺序表,根据实际需要动态的申请两倍或者n倍的空间,但实际上还是很容易造成空间浪费的情况发生,而且当我们对这个顺序表进行增删改查时要有时要挪动这个顺序表的所有元素,这就很容易造成效率低下的问题发生,而链表正是为了优化这个问题。

链表就像是上面挂满了一个个珠子的手串一样,上面的每一个珠子就是链表的一个个节点:

这样我们就可以根据不同人不同手腕的大小(不同的需求)来满足各种不同的需求,链表的这种结构使得我们可以更加精细化的申请动态内存空间,减少空间的浪费。


1.1单链表的结构体定义

既然我们要实现这种结构,那我该如何将这一个个看似没有联系的节点链接到一起呢?这个时候就需要运用的我们之前学习过的结构体,我们可以定义一个单链表结构体,并在里面定义两个结构体变量,一个结构体变脸用于存放数据,第二个结构体变量用于存放下一个结构体变量的地址,这样我们就可以通过第二个结构体变量成员找到下一个节点的位置:

cpp 复制代码
typedef int SLTDataType;
//定义结构体
typedef struct SListNode {
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

但是需要注意的是这不是递归,递归是对同函数的重复运用并且有回归条件,而链表这是在不连续的空间上有不同地址的节点,通过结构体指针将它们联系在了一起:


1.2单链表的实现

还是和顺序表一样,同样是在一个空项目下面创建三个文件,分别用于存放定义单链表结构体存放与头文件声明函数、定义函数、测试:

在"SList.h"定义完单链表结构体变量后,可以在之后存放函数的声明,下面是我们要实现的功能:

cpp 复制代码
//打印单链表
void SLTPrint(SLTNode* pHead);
//申请节点
SLTNode* SLTBuyNode(SLTDataType x);
//尾插
void SLTPushBack(SLTNode** ppHead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** ppHead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** ppHead);
//头删
void SLTPopFront(SLTNode** ppHead);
//查找
SLTNode* SLTFind(SLTNode* pHead, SLTDataType x);
//指定位置之前插入数据
void SLTinsertBeforePosition(SLTNode** ppHead, SLTNode* pos, SLTDataType x);
//指定位置之后插入数据
void SLTinsertAfterPosition(SLTNode* pos, SLTDataType x);
//删除指定位置之前的数据
void deleteBeforePosition(SLTNode** ppHead, SLTNode* pos);
//删除指定位置之后的数据
void deleteAfterPosition(SLTNode* pos);
//销毁链表
void SLTDestroy(SLTNode** ppHead);

下面我们就可以在【SList.c】这个文件中来逐步实现我们的功能了

打印单链表:

cpp 复制代码
void SLTPrint(SLTNode* pHead)
{
	SLTNode* pust = pHead;
	while (pust)
	{
		printf("%d->", pust->data);
		pust = pust->next;//进入下一个节点
	}
	printf("NULL\n");
}

单链表的最后一个节点指向空指针NULL当pust为NULL条件判断为假指定跳出循环停止打印

申请节点:

cpp 复制代码
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newNode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newNode->data = x;
	newNode->next = NULL;
	return newNode;
}

通过malloc函数申请一个单链表节点的空间,将数据存入结构体中并返回新节点的地址

尾插:

cpp 复制代码
void SLTPushBack(SLTNode** ppHead, SLTDataType x)
{
	assert(ppHead);
	SLTNode* newNode = SLTBuyNode(x);
	if (*ppHead == NULL)//空链表
	{
		*ppHead = newNode;
	}
	else //非空
	{
		SLTNode* ptail = *ppHead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}//指向尾结点
		ptail->next = newNode;
	}
}

因为我们是对原链表进行操作所以这里穿的就是二级指针,当成功的申请到了存放要插入的数据的新节点时,我们要先判断这个链表是不是空链表,如果是空链表就直接将新节点作为开头的第一个有效节点,非空的话就通过循环遍历单链表找到最后一个节点,并修改尾节点中的结构体指针让它指向我们申请到的新节点

头插:

cpp 复制代码
void SLTPushFront(SLTNode** ppHead, SLTDataType x)
{
	assert(ppHead);
	SLTNode* newNode = SLTBuyNode(x);
	newNode->next = *ppHead;
	*ppHead = newNode;
}

将申请到的新节点的结构体指针指向原链表的第一个节点*ppHead,再将新节点的地址作为新的第一个节点的地址就完成了头插

尾删:

cpp 复制代码
void SLTPopBack(SLTNode** ppHead)
{
	assert(ppHead && *ppHead);
	//单节点
	if ((*ppHead)->next == NULL)
	{
		free(*ppHead);
		*ppHead = NULL;
	}
	//多节点
	else
	{
		SLTNode* palist = *ppHead;
		SLTNode* prev = *ppHead;
		while (palist->next)
		{
			prev = palist;
			palist = palist->next;
		}
		free(palist);
		palist = NULL;
		prev->next = NULL;
	}
}

同样需要判断原链表是非为空,为空就直接释放掉,不为空就找到尾节点,但是需要保存尾节点的地址,不然直接释放尾节点后就找不到尾节点里的结构体指针了,尾节点里的结构体指针同样置为空,所以我们通过备份的尾节点地址将里面的结构体指针指向为空

头删:

cpp 复制代码
void SLTPopFront(SLTNode** ppHead)
{
	assert(ppHead && *ppHead);
	SLTNode* nxt = (*ppHead)->next;
	free(*ppHead);
	*ppHead = nxt;
}

查找:

cpp 复制代码
SLTNode* SLTFind(SLTNode* pHead, SLTDataType x)
{
	SLTNode* pcur = pHead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

指定位置之前插入数据:

cpp 复制代码
void SLTinsertBeforePosition(SLTNode** ppHead, SLTNode* pos, SLTDataType x)
{
	assert(ppHead && *ppHead);
	assert(pos);
	SLTNode* newNode = SLTBuyNode(x);
	if (*ppHead == pos)
	{
		SLTPushFront(ppHead, x);
	}
	else
	{
		SLTNode* pevr = *ppHead;
		while (pevr->next != pos)
		{
			pevr = pevr->next;
		}//pevr指向pos前一个节点

		pevr->next = newNode;
		newNode->next = pos;
	}
}

当表为空时就可以直接调用头插,不为空就是就直接遍历到pos的前一个节点,再改变节点的指针指向就可以了

指定位置之后插入数据:

cpp 复制代码
void SLTinsertAfterPosition(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newNode = SLTBuyNode(x);
	//链接节点
	newNode->next = pos->next;
	pos->next = newNode;
}

删除指定位置之前的数据:

cpp 复制代码
void deleteBeforePosition(SLTNode** ppHead, SLTNode* pos)
{
	assert(ppHead && *ppHead);
	assert(pos);
	if (*ppHead == pos)
	{
		SLTNode* newNode = (*ppHead)->next;
		free(*ppHead);
		*ppHead = newNode;
	}
	else
	{
		SLTNode* prev = *ppHead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

和在链表之前插入数据很想,只不过变为了删除(释放动态内存空间)数据,这里我就不赘述了

删除指定位置之后的数据:

cpp 复制代码
void deleteAfterPosition(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* dsl = pos->next;
	pos->next = dsl->next;
	free(dsl);
	dsl = NULL;
}

销毁链表:

cpp 复制代码
void SLTDestroy(SLTNode** ppHead)
{
	assert(ppHead && *ppHead);
	SLTNode* pcur = *ppHead;
	while (pcur)
	{
		SLTNode* net = pcur->next;
		free(pcur);
		pcur = net;
	}
	*ppHead = NULL;
}

销毁链表不是只销毁第一个节点就好了,所以我们可以通过在销毁一个节点时保存这个节点里的结构体指针来防止找不到下一个要销毁的节点,再通过一个循环把整个单链表给销毁干净,不要忘了把第一个的节点的地址置为空


2.链表的分类

链表一般有如上图的2*2*2 = 8种链表,前面我们实现的单链表就属于不带头单向不循环链表,而下面要将的双向表与单链表相反属于带头双向循环链表,置于是什么意思将在下面会讲到。看到有这么对链表也不用焦虑,因为常用基本只有单链表与双向链表两种,就算要用到其他的链表相信会了这两种链表写其他的链表也是得心应手


3.双向链表的定义

前面我们实现的单链表可以更有效的利用空间,但对某个节点操作时总是要遍历链表,这样操作就会非常的繁琐,而双向链表就解决了这个问题。我们先来画图来展示双向链表的结构:

可以看到双向链表区别于单链表的地方就是多了一个头节点,这个头节点是固定的在进行增删查改时不能修改我们的头节点(哨兵位),而且多了一个结构体指针指向前一个节点,整个链表构成了一个循环的结构。这样就可以更加灵活的对某个节点进行操作,大大的减少了用来遍历的代码


3.1双向链表的结构体定义

前面我们知道了双向链表多了一个指向前面节点的结构贴指针,所有我们需要在双向链表结构体里多创建一个结构体指针成员用于指向前一个节点,而且还多了一个不能修改的头节点,所以我们还需要对双向链表进行初始化:

cpp 复制代码
typedef int LITDataType;
typedef struct LITNode
{
	LITDataType data;
	struct LITNode* next;
	struct LITNode* prve;
}LTNode;

下面是双向链表的实现,这是我们要实现的功能,可以同样防止【List.h】文件下:

cpp 复制代码
//创建节点
LTNode* BuyNode(LITDataType x);
//初始化
void LTInit(LTNode** pphead);
//打印双向链表
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LITDataType x);
//头插
void LTPushFront(LTNode* phead, LITDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFine(LTNode* phead, LITDataType* x);
//在指定位置之后插入
void LTInsert(LTNode* pos, LITDataType x);
//删除指位置数据
void LTErase(LTNode* pos);
//销毁双向链表
void LTDesTroy(LTNode* phead);

3.2双向链表的实现

还是同样在【List.c】文件下实现函数,因为有了头节点和双向指针所有双向链表的代码会比单链表的代码还要简单

创建节点:

cpp 复制代码
LTNode* BuyNode(LITDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	node->data = x;
	node->next = node->prve = node;
	return node;
}

初始时,这个新节点的前后指针都指向它自己,后期我们实现对双向链表的增删时只需要修改这个节点的前后指针进行修改就可以了

初始化:

cpp 复制代码
void LTInit(LTNode** pphead)
{
	*pphead = BuyNode(-1);
}

我们直接申请一个节点,最好是给他穿一个不会用到的值对这个链表进行初始化,后期我们正是基于这个节点对链表进行增删查改,所以后面就不用像单链表一样传二级指针了,只需要对这个节点的地址进行操作就可以了

打印双向链表:

cpp 复制代码
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

当遍历到头节点时便跳出循环停止打印

尾插:

cpp 复制代码
void LTPushBack(LTNode* phead, LITDataType x)
{
	assert(phead);
	LTNode* newnode = BuyNode(x);
	//处理newnode指针
	newnode->next = phead;
	newnode->prve = phead->prve;
	//处理双向链表指针
	phead->prve->next = newnode;
	phead->prve = newnode;
}

双向链表尾插可以通过头节点来确定尾插的位置,因为原链表的尾节点的next指针指向的是头节点,所以在我们想尾插一个新的节点时,可以先修改新节点的指针,因为这样可以不影响到原链表,然后再去修改原链表。但是想要注意的是【phead->prve->next = newnode;】与【 phead->prve = newnode;】是不能交换顺序的,如果先修改了phead->prve(原来的尾节点)那我们就无法找到原来尾节点的next指针了

头插:

cpp 复制代码
void LTPushFront(LTNode* phead, LITDataType x)
{
	assert(phead);
	LTNode* newnode = BuyNode(x);
	newnode->next = phead->next;
	newnode->prve = phead;
	//处理原链表
	phead->next->prve = newnode;
	phead->next = newnode;
}

头插是再头节点后面进行插入,我们是不能改变头节点的位置顺序的

尾删:

cpp 复制代码
void LTPopBack(LTNode* phead)
{
	assert(phead && phead->next != phead->prve);
	LTNode* del = phead->prve;
	del->prve->next = phead;
	phead->prve = del->prve;
	//释放内存
	free(del);
	del = NULL;
}

头删:

cpp 复制代码
void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	del->next->prve = phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}

查找:

cpp 复制代码
LTNode* LTFine(LTNode* phead, LITDataType* x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

遍历整个双向链表找到数据并返回节点地址,找不到就返回空指针NULL

在指定位置之后插入:

cpp 复制代码
void LTInsert(LTNode* pos, LITDataType x)
{
	assert(pos);
	LTNode* newnode = BuyNode(x);
	newnode->next = pos->next;
	newnode->prve = pos;
	//处理原链表
	pos->next->prve = newnode;
	pos->next = newnode;
}

这个函数和尾插很相似,这是因为双向链表是一个循环的链表,所以不管是哪个位置都可以直接使用尾插来搞定

删除指位置数据:

cpp 复制代码
void LTErase(LTNode* pos)
{
	assert(pos);

	pos->next->prve = pos->prve;
	pos->prve->next = pos->next;
	free(pos);
	pos = NULL;
}

只需要修改要删除的节点的前后指针,然后再将该节点释放掉就可以了

销毁双向链表:

cpp 复制代码
void LTDesTroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* nex = pcur->next;
		free(pcur);
		pcur = nex;
	}
	free(phead);
	phead = NULL;
}

和单链表一样,需要不断的遍历整个链表将整个链表节点逐一释放掉就可以了


相关推荐
浅念-3 小时前
链表经典面试题目
c语言·数据结构·经验分享·笔记·学习·算法
czwxkn4 小时前
数据结构-线性表
数据结构
tobias.b4 小时前
408真题解析-2010-1-数据结构-栈基础操作
数据结构·408真题解析
菜鸟233号4 小时前
力扣213 打家劫舍II java实现
java·数据结构·算法·leetcode
方便面不加香菜4 小时前
数据结构--栈和队列
c语言·数据结构
Pluchon5 小时前
硅基计划4.0 算法 动态规划进阶
java·数据结构·算法·动态规划
2401_841495647 小时前
【Python高级编程】单词统计与查找分析工具
数据结构·python·算法·gui·排序·单词统计·查找
-To be number.wan7 小时前
【数据结构真题解析】哈希表高级挑战:懒惰删除、探测链断裂与查找正确性陷阱
数据结构·算法·哈希算法
Qhumaing8 小时前
数据结构——例子求算法时间复杂度&&空间复杂度
数据结构·算法