数据结构之双向链表

单链表中我们详细讲解了单向链表的创建和相关操作。

本篇文章,博主将继续为大家介绍链表的另一种结构---双向链表中的双向带头循环链表。

双向链表的结构特点

在经过对单链表的学习后,大家可能觉得双向链表是一座难以逾越的大山。

但实际上,双向链表的实现更加的简单。

何为双向? 

顾名思义,通过当前结点不仅可以找到前一个结点,也能找到后一个结点。

 

在上图中我们可以看到带头双向循环链表有几个特殊点。

1.需要定义一个哨兵卫作为头,并且哨兵中不存储数据信息。 


2.在定义结构体时,不同于单向链表只需定义一个指向下一个节点的指针,还需定义一个指向前一个节点的指针。


 3.尾结点的下一个指向要指向哨兵卫头节点,头节点的上一个结点指向尾结点,以达到循环的效果。因此如果想要链表不循环,只需要使尾节点cur->next=null。


4.在对链表进行增删查改操作时,我们不需要传递二级指针,因为哨兵卫节点帮我们保存着第一个位置的地址。 

双向链表初始化

首先,我们定义节点的结构。上代码。

typedef int Datatype;
typedef struct ListNode
{
	struct ListNode* next;//指向下一个节点的指针
	struct ListNode* prev;//指向前一个结点的指针
	Datatype data;//节点存储的数据
}LTNode;

 不同于单链表,双链表建立前需要先定义一个头节点作为哨兵卫。

LTNode* LTInit()
{
	LTNode* phead = BuyNode(-1);//开辟空间,不保存有效数据
	phead->prev = phead;//哨兵卫的上一个节点指向自己
	phead->next = phead;//哨兵卫的下一个节点指向自己
	return phead;
}

 由于此时链表中只有一个哨兵卫节点,所以要先使其本身形成循环。

这里BuyNode大家是不是很熟悉呢!在单链表实现中,我们建立一个外部函数来简化开创新空间的代码,双链表也不例外。在这里我们再次复习一下吧。

LTNode* BuyNode(Datatype x)

{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//申请空间
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

双向链表的数据插入

链表初始化后,我们来实现数据的插入。同样的,数据的插入分为头插和尾插。

需要注意的是,双向链表的数据插入要关注节点的前后指向。

头插的实现

void LTPushFront(LTNode* phead, Datatype x)
{
	assert(phead);
	LTNode* newnode = BuyNode(x);//申请空间
	newnode->next = phead->next;//插入的节点指向头节点的下一个节点
	phead->next->prev = newnode;//头节点的上一个节点与插入节点相连
	phead->next = newnode;//头节点与插入节点相连
	newnode->prev = phead;//插入节点的prev指向头节点,完成节点的双向实现
}

博主将画一个丑陋的图便于大家理解。

需要注意的点是,我们得先使newnode指向P1再使P1指向newnode。

 

尾插的实现

尾插和头插的实现类似,不做赘述,直接上代码。

void LTPushBack(LTNode* phead, Datatype x)
{
	assert(phead);
	LTNode* tail = phead->prev;//找尾
	LTNode* newnode = BuyNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

双向链表的数据打印

实现数据插入后,我们打印来验证插入的正确性。

void LTprint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	printf("guard=>");
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

void TextList1()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPushBack(plist, 5);
	LTPushBack(plist, 6);
	LTprint(plist);
}
void TextList2()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPushFront(plist, 5);
	LTPushFront(plist, 6);
	LTprint(plist);
}

打印结果如下。

 

双向链表数据的删除

 同样的,删除分为头删和尾删。

尾删的实现

void LTPopBack(LTNode* phead)
{
	assert(phead);
    assert(phead->next!=phead);
	LTNode* tail = phead->prev;//找尾
	LTNode* tailprev = tail->prev;//换尾
	free(tail);//释放原来的尾
	tailprev->next = phead;//新的尾与哨兵卫相连
	phead->prev = tailprev;
}

注意这里的断言,由于我们的哨兵卫头节点是不能删除的,所有当链表中只有一个哨兵卫时不能进行删除操作,头删时亦是如此。 

头删的实现 

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTNode* first = phead->next;
	LTNode* second = first->next;
	phead->next = second;
	second->prev = phead;
	free(first);
}

头删时需要注意的时,我们不能过早的将first指针空间释放,否则second指针会找不到phead。正确的顺序是,先建立起second与phead间的联系后再释放first。

 双向链表的查找

双向链表的查找与单链表思想相同,直接上代码。

LTNode* LTFind(LTNode* phead, Datatype x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

双向链表的销毁 

在双向链表的销毁中,我们需要在释放空间前保存当前节点的下一个节点,否则会导致释放当前节点后找不到后续节点的地址。 

此外,不要忘记释放头节点的空间哦!

void LTDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;//保存下一个节点地址
		free(cur);//释放当前节点
		cur = next;
	}
	free(phead);//释放哨兵卫
	phead = NULL;
}

指定位置的插入和删除

在指定节点pos前插入


void LTInsert(LTNode* pos, Datatype x)
{
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;
	LTNode* newnode = BuyNode(x);
	newnode->prev = posprev;
	newnode->next = posnext;
	pos->prev = newnode;
	posprev->next = newnode;
	
}

删除指定节点 

void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);

}

总结

本篇文章结束后,对于链表的学习就告一段落了。在试题中有很多题目会针对链表进行设计,希望大家能够理解并进行实操。 

 

 

 

 

 

 

 

 

 

 

 

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值