简单手撕双向循环带头链表

1.什么是双向带头循环链表?

双向带头循环链表是数据结构里众多链表中的一种,这种类型的链表是一种非常常用的链表。他的整体结构是首先在最开始是有一个哨兵位来当作一个头节点,这个头节点是不需要存放数据的(当然想存放数据也是可以的)。在我们使用这种类型的链表的时候,由于有这个哨兵位头节点的存在,因此我们就不需要来考虑链表是否为空的问题,在使用的过程中更加方便;其次,对于这种链表,他每一个节点都是有一个前驱指针和后继指针,这里我就把前驱指针叫做prve,后继指针叫做next。他的prve指针指向的是前一个节点的地址,next指针指向的是下一个节点的地址。但是双向带头循环链表中,由于要实现他"循环"的性质,因此头节点的prve指针指向的是链表中最后一个节点,而最后一个节点的next指针指向的是头节点

这就构成了双向带头循环链表。

2.双向带头循环链表的实现

1.链表的初始化

要完成这个链表的初始化,首先在对于节点的定义上我们就比单链表多了一个前驱指针prve指针。

那对于初始化,既然要实现一个双向带头循环的,那首先就得把这个头节点给创建出来,然后再让他的next和prve指针都指向自己。

大概就如上图所示。

那代码的实现就非常简单了。

DL* DLInit()
{
	DL* phead = DListCreatBuyNode(-1);
	phead->next = phead;
	phead->prve = phead;
	phead->val = 0;
	return phead;
}

这段代码的思路就是首先创建一个头节点,这个头节点我把-1存放到了里面,然后让头节点的next指向自己,让prve指向自己。而创建节点的代码可以把他写到一个函数里面,如下。

DL* DListCreatBuyNode(DListDataType val)
{
	DL* node = (DL*)malloc(sizeof(DL));
	if (node == NULL)
	{
		perror("newnode malloc fail!");
		return;
	}
	node->val = val;
	node->next = NULL;
	node->prve = NULL;
	return node;//最后返回malloc出来的节点
}

2.链表的尾插

对于链表的尾插,我们首先要想清楚他的逻辑,如果想要实现尾插的话,首先得找到最后一个节点,俗称找尾,找到了尾节点之后,再进行尾插,在这里的尾插和在单链表上的尾插不一样,因为要时刻保证他是一种双向带头循环的结构,要保证他是循环的,因此在尾插的时候还是有比较多需要注意的地方。

首先来说一下他尾插的整体思路:

如果想实现一个双向链表的尾插,就首先是需要找到该链表的尾节点

那该怎么找到这个尾节点?在双向循环链表中,整个链表都是循环的,因此不能像单链表那样把寻找尾的判断条件变成tail->next==NULL,这样在双向循环链表中,这个循环是永远都出不来了的。因此,我们可以考虑把条件改成如果tail->next!=head的话,就继续寻找链表中的尾节点,如果找到了尾节点,就用尾节点的next指向插入的节点,然后用新插入节点的prve指向尾节点,新节点的next指向哨兵位,哨兵位的prve指向新插入的节点。这个就是尾插的大体思路。

void DLPushBack(DL* phead, DListDataType val)
{
	DL* newnode = DListCreatBuyNode(val);
	DL* newhead = phead;
	DL* Tail = phead->prve;
	Tail->next = newnode;
	newnode->prve = Tail;
	newnode->next = newhead;
	newhead->prve = newnode;
}

3.链表的头插

那对于链表的头插来说,如果在这里我们没有用哨兵位头节点的话,就得判断链表是否为空的状态。但在这里是已经有了头节点,那么这个链表里面就不存在链表为空的情况,是一直都有东西的。因此我们直接把一个新的节点插入到头节点哨兵位的后面就可以了。

那总体思路就是: 创建一个新节点,让新节点node->next指向phead->next,phead->next->prve指向node,phead->next指向node,node->prve指向phead。ok打完收工!

void DLPushFornt(DL* phead, DListDataType val)
{
	DL* newnode = DListCreatBuyNode(val);
	DL* cur = phead->next;
	newnode->next = cur;
	cur->prve = newnode;
	phead->next = newnode;
	newnode->prve = phead;
}

在这里以防混淆,我把phead的next用cur来代替~

4.链表的尾删

而双向链表的尾删,大体思路则是首先找到尾节点,让尾节点前一个的next指向哨兵位头节点,让哨兵位头节点的next指向tail->prve。最后再把尾节点给释放掉。

void DLPopBack(DL* phead)
{
	assert(phead->next!=phead);
	DL* Tail = phead->prve;
	DL* newprve = Tail->prve;
	DL* newhead = phead;
	newprve->next = newhead;
	newhead->prve = newprve;
	free(Tail);
}

5.链表的头删

而对于链表的头删,就是把哨兵位头节点的下一个节点释放掉。

以这张图为例,如果我想把cur节点给释放掉,那就得让phead->next指向curnext,curnext->prve指向phead,最后把cur节点给释放掉~

void DLPopFront(DL* phead)
{
	assert(phead->next != phead);
	DL* cur = phead->next;
	DL* next = cur->next;
	DL* newhead = phead;
	newhead->next = next;
	next->prve = newhead;
	free(cur);
}

6.在指定节点位置插入一个新的节点

其实这个在指定位置插入节点的逻辑跟头插是差不多的,只不过从改动phead变成了改动链表中节点的链接关系。而由于链表是malloc出来的,每一个节点的地址都是随机的,因此不能像数组那样对下标操作,从而进行准确的插入,在链表中,只能根据值来进行插入,因而在"指定位置插入"其实并不完全指定位置,只能是一个比较模糊的概念。

就比如上面这张图,如果我想把node节点插入到curnext这个位置的话,那么我就可以先让cur->next指向node,node->next指向curnext,curnext->prve指向node,node->prve指向cur。而在传参的时候,我们可以传第一个是链表地址,第二个是想修改的节点的值,第三个则是新节点。

代码如下:

void DLInsert(DL* phead, DListDataType pos, DListDataType val)
{
	assert(phead != NULL);
	DL* pHead = phead->next;
	DL* pTail = phead->next;
	DL* InsertNode = DListCreatBuyNode(val);
	DL* FindNode = DSLTFindNode(phead, pos);
	while (pTail != phead)
	{
		if (pTail == FindNode)
		{
			break;
		}
		pHead = pTail;
		pTail = pTail->next;
	}
	InsertNode->next = pTail;
	pHead->next = InsertNode;
	pTail->prve = InsertNode;
	InsertNode->prve = pHead;
}

7.删除指定节点

比如我想删除node节点,那么在删除node节点的时候,我们可以把curnext->next指向tail,tail的prve指向node,node->next 指向tail,node->prve指向curnext。最后再把node节点给free掉就行了

void DLPop(DL* phead, DListDataType pos)
{
	assert(phead->next != phead);
	DL* pHead = phead->next;
	DL* Del = DSLTFindNode(phead, pos);
	DL* pTail = phead->next;
	//DL* cur = phead->next;
	if (phead->next == Del)
	{
		DLPopFront(phead);
		return;
	}
	while (pTail != phead)
	{
		if (pTail == Del)
		{
			break;
		}
		pHead = pTail;
		pTail = pTail->next;
	}
	pHead->next = pTail->next;
	pTail->next->prve = pHead;
}

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值