数据结构初阶(c语言)-双向链表

这里首先纠正上篇文章一个错误,链表的一个有效数据点应该称为结点而不是节点。

一,双向链表的概念与结构

1.1概念与结构示意图

         我们所说的双向链表全称为带头双向循环链表,也就是说此链表带有哨兵位结点(不存放任何数据的结点,且为头结点)。图示结构如下:

         注意:这里的“带头”跟前面我们说的“头结点”是两个概念,实际前面的在单链表阶段称呼不严谨。

1.2双向链表结构代码

typedef int LTNDataType;
typedef struct ListNode
{
	LTNDataType val;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

          prev为指向前一个结点的指针,next为指向后一个结点的指针,val为该结点中存储的数据。

二,实现双向链表的功能

2.1创建链表节点函数LTBuyNode

          由于我们的双向链表在没有存放任何数据时,还有一个哨兵位结点,所以我们需要先实现链表结点创建函数:

LTNode* LTBuyNode(LTNDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("buynode:");
		exit(1);
	}
	newnode->val = x;
	newnode->next = newnode->prev = newnode;
	return newnode;
}

          我们的双向链表为循环链表,所以在只有一个结点时,我们创建的新结点需要自己的两个前后指针均指向自己,以便实现我们的双向链表初始化。

2.2双向链表的初始化函数LTInit

         由于我们的哨兵位结点不存放有效数据,所以我们需要给初始结点直接返回一个存放数据为-1(无效数据)的结点,作为双向链表的初始结点:

LTNode* LTInit()
{
	LTNode* pcur = LTBuyNode(-1);
	return pcur;
}

 2.3双向链表的尾插函数LTPushBack

void LTPushBack(LTNode* phead, LTNDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	phead->prev->next = newnode;
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev = newnode;
}

         先创建一个新结点并用临时变量进行接收,下一步则是使尾节点的next指向我们的新结点,同时将新结点的prev指向先前的尾结点,接下来由于我们创建的新结点为当前的尾结点,所以我们需要让其next指针指向哨兵位结点,最后再使哨兵位结点的prev指向我们的新结点即完成我们的尾插函数。

2.4双向链表的头插函数LTPushStart

         这里需要注意,如果我们是直接将数据插到哨兵位前面,我们的方法实际上此时与尾插法无异,所以要实现头插,我们则需要将新结点直接插入到哨兵位结点的后面,从而实现头插:

void LTPushStart(LTNode* phead, LTNDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead;
	newnode->next = phead->next;
	phead->next = newnode;
	newnode->next->prev = newnode;
}

          为了插入数据时的方便,我们先改变新插入结点的next与prev,这样不会对当前链表的结构产生影响,接下来我们便改变哨兵位结点的next与原哨兵位结点的next结点的prev,使其均指向我们创建的新结点,实现我们的头插法。

2.5判空函数LTEmpty

         当我们的链表只剩下哨兵位结点时,我们判定此时链表不存放任何有效数据,又由于我们此时只有一个结点,所以我们直接判定当前哨兵位结点的prev是否指向它自己即可:

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return (phead->next == phead);
}

2.6尾删函数LTDeltBack

         在进行尾删之前,我们也需要对链表进行判空,然后我们需要先用临时变量去保存尾结点的地址,同时改变尾结点的前结点的next,使其指向哨兵位,然后改变哨兵位结点的prev使其指向我们当前尾结点的前一个结点,最后释放尾结点即可:

void LTDeltBack(LTNode* phead)
{
	assert(phead && !LTEmpty(phead));
	LTNode* pcur = phead->prev;
	phead->prev->prev->next = phead;
	phead->prev = phead->prev->prev;
	free(pcur);
	pcur = NULL;
}

2.7头删函数LTDeltStart

         与尾删函数类似,也需要创建临时变量去记住要释放结点的位置,然后接下来步骤的思想与尾删基本相同:

void LTDeltStart(LTNode* phead)
{
	assert(phead && !LTEmpty(phead));
	LTNode* pcur = phead->next;
	phead->next = phead->next->next;
	phead->next->next->prev = phead;
	free(pcur);
	pcur = NULL;
}

2.8寻找目标位置函数LTFind

          与单链表的寻找方法基本一致,不过要注意终止条件应该是遍历到哨兵位结点时停止:

LTNode* LTFind(LTNode* phead,LTNDataType x)
{
	assert(phead && !LTEmpty(phead));
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->val == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	printf("没有找到\n");
	return NULL;
}

2.9删除指定位置和向指定位置之后插入数据函数LTDeltDesBack与LTInserDesBack

         与单链表一样,但是这里向指定位置之前或之后插入数据方法一致,只是你实现之后插入后,如果想实现之前,只需要去用prev寻找上一结点即可:
LTDeltDesBack:

void LTDeltDesBack(LTNode* pos,LTNode* phead)
{
	assert(pos && (pos->next != phead));
	LTNode* pcur = pos->next;
	pos->next->next->prev = pos;
	pos->next = pos->next->next;
	free(pcur);
	pcur = NULL;
}

LTInserDesBack:

void LTInserDesBack(LTNode* pos,LTNDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = pos;
	newnode->next = pos->next;
	newnode->next->prev = newnode;
	pos->next = newnode;
}

2.10销毁链表函数DestoLTN

void DestoLTN(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	LTNode* pcur1 = NULL;
	while (pcur != phead)
	{
		pcur1 = pcur->next;
		free(pcur);
		pcur = pcur1;
	}
	free(phead);
}

三,顺序表与链表的对比

 

         当我们学习完顺序表与链表之后,下篇文章我们将介绍栈与队列的实现,在理解顺序表与链表功能的实现后,栈和队列的实现非常简单,仅仅只会有概念上不同的问题,我们下篇文章见。

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值