C语言双向链表的含义与链表数据操作代码详解!

本文详细介绍了C语言中双向链表的含义,包括其结构、哨兵位、双向性以及循环特性。提供了双向链表的初始化、打印、节点创建、查找、插入、删除和销毁等一系列操作函数的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言:于本文中,我们将讲到C语言数据结构中的双向链表的含义,以及对于双向链表的增删查改函数。该函数相比于单向链表,反而还更加的简单。希望这篇文章可以对你有帮助,也希望同样热爱代码编程的你能给我支持,您的支持就是我最大的动力!
关于顺序表以及单链表等更多数据结构知识详解可前往个人主页: 计信猫

目录

一,双向链表的含义

二,双向链表结构体的定义

三,双向链表数据操作预备函数

1,双向链表初始化函数

2,双向链表打印函数

3,双向链表节点创建函数

4,双向链表查找函数

四,双向链表数据操作函数

1,双向链表尾插函数

2,  双向链表头插函数

3,双向链表尾删函数

 4,双向链表头删函数 

5,双向链表pos位置之后插入数据

6,双向链表删除pos节点函数

7,双向链表销毁函数

一,双向链表的含义

        双链表的含义为带头双向循环链表,可以由下图表示:

        带头:所谓的带头,其实也就是图中的head,它也同时被称为哨兵位。我们在之前单链表中所用到的头节点其实也只是为了我们方便单链表的讲解才这么称呼的,而实际上,头节点其实就是指的图中的head,也就是哨兵位。然而哨兵位不会储存任何数据,同时它也不能改变,它在单链表中只是起着领头的作用。

        双向:双向就意味着,相较于单链表,双链表的每一个节点都会额外储存一个新的结构体地址指针,也就是pre,用于记录该节点前一个节点的地址,从而实现双向的功能。

        循环双链表中的最后一个节点会与头节点(哨兵位)首尾相连。以此形成循环功能。

二,双向链表结构体的定义

        在我们前文学习了的单链表的结构体定义之后,其实双链表的结构体定义也就十分简单了。双链表节点=所储存的数据+指向下一个节点的指针+指向上一个节点的指针

        所以我们仍然使用三文件操作法,于新的项目中创造List.h,List.c以及test.c文件

        而在List.h头文件中我们便如下定义结构体:

#pragma once
//包含所会用到的头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义结构体
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;//双链表所储存的数据
	struct ListNode* next;//后一个节点的地址
	struct ListNode* pre;//前一个节点的地址
}LT;

        注:对于双向链表中,空链表意味着只有一个哨兵位,并不是全部为空! 

三,双向链表数据操作预备函数

        在真正的学习双向链表操作函数前,我们需要先学习三个预备函数,以方便我们后续的双向链表数据操作函数的实现以及检验。它们分别为双向链表的初始化函数,双向链表的打印函数,双线链表的节点创建函数以及双向链表的查找函数。

1,双向链表初始化函数

        当我们想创造一个双向链表时,则需要先创造一个哨兵位,而哨兵位的创建其实非常简单,我们只需要使用malloc函数申请一个结构体的空间就可以了。所以我们的代码如下:

//双向链表的初始化
void LTInit(LT** pphead)
{
	//给双链表创造一个哨兵位
	*pphead = (LT*)malloc(sizeof(LT));
    (*pphead)->next = (*pphead)->pre = *pphead;
}

2,双向链表打印函数

        在我们之后的代码实现中,我们便可以使用该函数来对双向链表中的数据进行打印,从而判断函数是否实现成功。

        该函数中,我们定义一个新的变量pcur来表示哨兵位之后的一个节点,之后我们使用while循环遍历链表,并且在pcur->next!=phead的条件为假的情况下跳出。所以我们的打印代码如下:

//双向链表的打印
void LTPrint(LT* phead)
{
	LT* pcur = phead->next;//创建pcur来遍历双向链表
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

3,双向链表节点创建函数

        在之后我们所要实现的头插,尾插的函数中,我们都需要先创建好一个节点,然后才能进行插入操作。而这个函数,就解决了这个问题。

        这个函数非常简单,我们所需要做的就是使用malloc函数申请一个节点的空间,再使创造出来的节点的next与pre指针全部指向自己就可以了。所以代码如下:

//双向链表节点的创建
LT* LTCreat(LTDataType x)
{
	LT* newnode = (LT*)malloc(sizeof(LT));//创建新节点
	if (newnode == NULL)//防止节点创建失败
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	//将新节点的头与尾均与自己连接
	newnode->next = newnode;
	newnode->pre = newnode;
	return newnode;
}

4,双向链表查找函数

        当我们需要查找链表中是否存在一个值x,如果有,则返回储存该数值的节点的地址;反之则返回NULL时,我们就会用到该函数。

        这也非常简单,我们只需要创建一个pcur变量,使用while循环来遍历整个链表,并在找到x值时就return pcur,如果没找到,就return NULL即可。那么我们的代码如下:

//双向链表的查找
LT* LTFind(LT* phead, LTDataType x)
{
	LT* pcur = phead->next;//创建一个pcur来遍历链表
    while (pcur != phead)
	{
        //找到了
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
    //未找到
	return NULL;
}

四,双向链表数据操作函数

        提要:1,因为我们之后所用到的函数大多不会对所用到的节点(哨兵位,pos节点)有所改变,所以我们传一级指针即可。而对于删除和摧毁函数,为减少记忆成本,保持接口一致性,我们也使用一级指针传址

1,双向链表尾插函数

        图解如下:

        依据图像我们可以看出,我们创造了一个新节点之后,我们只需要先将newnode的头与尾分别与d3,head相连,之后再将phead->next赋值为newhead,d3->next赋值为newnode而比较巧妙的地方就在于d3可以使用phead直接表示,不用专门去寻找。所以我们的代码如下:

//双向链表的尾插
void LTPushBack(LT* phead, LTDataType x)
{
	assert(phead);//哨兵位不可以为空
	LT* newnode = LTCreat(x);
	//先连接newnode
	newnode->next = phead;
	newnode->pre = phead->pre;
	//再连接phead与尾节点
	phead->pre->next = newnode;
	phead->pre = newnode;
}

2,  双向链表头插函数

        图解如下:

        如图所示,该函数与尾插类似,都是先创建一个新节点,后先将新节点newnodehead,d1连接,之后再将phead->next赋值为newnode,d1->pre赋值为newnode并且d1可以使用phead进行表示。所以代码如下:

//双向链表的头插
void LTPushFront(LT* phead, LTDataType x)
{
	assert(phead);//哨兵位不可以改变
	LT* newnode = LTCreat(x);
	//先连接newnode
	newnode->next = phead->next;
	newnode->pre = phead;
	//再连接phead与d1
	phead->next->pre = newnode;
	phead->next = newnode;
}

3,双向链表尾删函数

        图例如下:

        根据图我们可以分析得出,想要做到尾删,我们需要先将d2phead首尾连接起来,再使用free函数去释放掉d3(也就是尾节点)的空间。如果将刚才的顺序调换,那么则可能会导致d2无法被找到的问题。所以我们的代码如下:

//双向链表的尾删
void LTDeleteBack(LT* phead)
{
	//链表必须存在且不可以为空
	assert(phead && phead->next!=phead);
	LT* del = phead->pre;//尾节点定义为del(要被删除的节点)
	del->pre->next = phead;
	phead->pre = del->pre;
	free(del);//释放掉尾节点
	del = NULL;//防止野指针的出现
}

 4,双向链表头删函数 

        图解如下:

        所以如果我们想实现该函数,那我们也需要向前一个函数一样,先将pheadd2连接起来,再释放掉d1的空间。该过程不可以逆转,不然会导致d2无法找到的问题。故代码如下:

//双向链表的头删除
void LTDeleteFront(LT* phead)
{
	//链表必须存在且不可以为空
	assert(phead && phead->next != phead);
	LT* del = phead->next;
	del->next->pre = phead;
	phead->next = del->next;
	free(del);//释放掉d1
	del = NULL;//防止野指针的出现
}

5,双向链表pos位置之后插入数据

        图解如下:

        所以当我们想要在pos之后插入数据时,我们需要先使用LTCreat函数创建一个节点newnode,然后再将newnode的前后指针分别赋值为pos->nextpos->next->pre,后再将pos的后指针与pos->next的前指针赋值为newnode。当然,想要找到pos,我们则需要我们前面讲到的LTFind函数。代码如下:

//双向链表pos位置之后插入数据
void LTInsertAfter(LT* pos, LTDataType x)
{
	assert(pos);//对pos有规定,不可以为空
	LT* newnode = LTCreat(x);
	//先对newnode进行操作
	newnode->next = pos->next;
	newnode->pre = pos;
	//后对另外两个节点进行操作
	pos->next->pre = newnode;
	pos->next = newnode;
}

6,双向链表删除pos节点函数

        图解如下:

        老样子,我们要遵循先连后删的规律,我们要先将pos->prepos->next的两个节点首尾相连,然后再删去pos节点该函数就是如此简单。代码如下:

//双向链表删除pos节点
void LTDeletePos(LT* pos)
{
	//理论上pos不为空和phead不可以等于pos,但无phead参数,所以无法检验
	assert(pos);
	pos->next->pre = pos->pre;
	pos->pre->next = pos->next;
	//删除pos节点
	free(pos);
	pos = NULL;
}

7,双向链表销毁函数

        该函数十分的简单,我们所需要做的就是创建两个结构体指针pcurnext,分别用于while循环中遍历双向链表和记录被删除的节点的下一个节点的位置,一离开while循环,那也同时标志着双向链表被销毁。但我们也需要记住,phead指针需要在主函数(main函数)中手动置空。所以该函数的代码入下:

//双向链表的销毁
void LTDestroy(LT* phead)
{
	assert(phead);
	LT* pcur = phead->next;
	while (pcur != phead)
	{
		LT* next = pcur->next;//保存被删除节点的下一个节点的地址
		free(pcur);//释放pcur节点
		pcur = next;
	}
	pcur = NULL;
	//最后勿忘在主函数中对phead进行手动置空
}

        

         

         

         

     

         

         

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值