【数据结构】C语言实现带头双向循环链表

        在前面的博客中,我们学习了最简单的链表类型——单向、不带哨兵位、不循环,今天我们要来学习的是具有链表中最复杂的结构类型——双向、带哨兵位、循环的链表。我们先来看一下两者的结构示意图。

        注:头和哨兵位为同一个东西,下面均以哨兵位称呼。

 

        从图中我们不难发现,两个链表的结构简直是天差地别,第二种比第一种复杂太多了,那么第二种的实现同样会比第一种的实现难上很多吗?答案是否定的,虽然第二种的结构更加复杂,但是它的结构可以说是链表所有结构类型中最优秀的, 它的实现也是链表所有结构类型中最容易完成的,如果你不相信,通过下面的学习,你会发现它的优秀之处。


双向链表的实现

1.节点的定义

#define ListDataType int

typedef struct ListNode
{
	struct ListNode* prev;
	ListDataType data;
	struct ListNode* next;

}ListNode;

        双向链表的节点比单链表的节点多定义了一个prev指针,指向该节点前面的节点(如果是哨兵位则指向链表的最后的一个节点,达到循环的效果),别小看这多出来的一个prev,正是因为它才让双向链表比单链表更加优秀,我们可以通过prev直接找到该节点的上一个节点而不用再通过遍历的方式来寻找,这直接节省了尾插、尾删等的时间消耗。、

2.链表的初始化

        因为我们的双向链表带有哨兵位,那么我们就有必要通过初始化来为链表创造一个哨兵位。

ListNode* ListInit(void)
{
	ListNode* SentinelNode = (ListNode*)malloc(sizeof(ListNode));
	if (SentinelNode == NULL)
	{
		perror("malloc");
		exit(-1);
	}

	//开始时,哨兵位的next和prev都指向自己
	SentinelNode->next = SentinelNode;
	SentinelNode->prev = SentinelNode;

	return SentinelNode;
}

        哨兵位内不存储有效数据,只是单纯的方便对链表进行操作,哨兵位的存在可以帮助我们来减少一些特殊情况的判断,如头插、尾插的时候不用再判断链表是否为空来决定是否要改变plist(指向链表头的指针)的指向了,并且让链表传入接口时不用再传入二级指针了

3.新节点的创造

        在头插、尾插等多处我们都需要创造新节点,所以我们可以单独将创造节点的功能分割出来封装成一个函数,这样使用就更加方便,程序也不会显得很臃肿。

static ListNode* ListNodeCreat(const ListDataType val)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(-1);
	}

	newnode->data = val;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

4.尾插

void ListPushBack(ListNode* plist, const ListDataType val)
{
	assert(plist);

	ListNode* newnode = ListNodeCreat(val);

	ListNode* tail = plist->prev;
	tail->next = newnode;
	newnode->prev = tail;

	plist->prev = newnode;
	newnode->next = plist;

}

        有了prev我们再也不需要通过遍历找尾了,因为plist->prev就是我们需要找的尾,这是双向且循环给我们带来的好处,找到尾后的插入就很简单了。

5.尾删

void ListPopBack(ListNode* plist)
{
	assert(plist);
    //判断链表是否为空(不算哨兵位)
	assert(plist->next != plist);

	ListNode* tail = plist->prev;
	ListNode* TailPrev = tail->prev;

	free(tail);
	tail = NULL;

	TailPrev->next = plist;
	plist->prev = TailPrev;
}

        找到尾后的尾删同样简单,不过不要忘记判断链表内是否有节点可删(哨兵位不可删)。我们可以通过定义多个变量来方便操作,不要担心多定义变量的消耗,这几个变量对内存的消耗都是常数级的,只要不是那种内存有严格限制的机器都不用担心这些内存的消耗。

6.头插

void ListPushFront(ListNode* plist, const ListDataType val)
{
	assert(plist);

	ListNode* newnode = ListNodeCreat(val);

	ListNode* HeadNext = plist->next;

	plist->next = newnode;
	newnode->prev = plist;
	newnode->next = HeadNext;
	HeadNext->prev = newnode;
}

        哨兵位的存在不用让我们不用再判断是否要改plist,这样我们就不需要再针对特殊情况再单独写一种处理方法,也不用再传入二级指针。

7.头删

void ListPopFront(ListNode* plist)
{
	assert(plist);
	assert(plist->next != plist);

	ListNode* HeadNode = plist->next;
	ListNode* HeadNodeNext = HeadNode->next;

	free(HeadNode);
	HeadNode = NULL;
	plist->next = HeadNodeNext;
	HeadNodeNext->prev = plist;
}

        我们要记住,我们说的头都是不包括哨兵位的时候的头节点,即哨兵位的下一个节点。

8.链表的打印

void ListPrint(const ListNode* plist)
{
	assert(plist);

	ListNode* cur = plist->next;

	while (cur != plist)
	{
		printf("%d ", cur->data);

		cur = cur->next;
	}

	printf("\n");
}

         因为哨兵位内不存储有效数据,所以我们打印链表都是从哨兵位的后一个节点开始向后遍历,然后当cur通过循环结构回到哨兵位时停止打印循环。

9.在链表内查找特定数据

ListNode* ListFind(const ListNode* plist, const ListDataType val)
{
	assert(plist);

	ListNode* cur = plist->next;
	while (cur != plist)
	{
		if (cur->data == val)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

        查找的思路与打印的思路很像,都是从哨兵位的后一个节点开始到哨兵位结束,找到了就返回目标节点的地址,没找就返回NULL。

10.在目标节点前插入节点

        在我们实现无头单向不循环链表时,在目标节点前插入节点相当麻烦,但是在我们现在这个非常优秀的结构下,这个接口的实现不过是小菜一碟。

void ListInsert(ListNode* pos, const ListDataType val)
{
	assert(pos);

	ListNode* newnode = ListNodeCreat(val);

	ListNode* PosPrev = pos->prev;

	PosPrev->next = newnode;
	newnode->prev = PosPrev;
	newnode->next = pos;
	pos->prev = newnode;
}

        我们现在短短数行就完成了当初需要许多麻烦处理的接口,这难道不能说明这个结构的优秀吗?

11.删除目标节点

void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* PosPrev = pos->prev;
	ListNode* PosNext = pos->next;

	free(pos);
	pos = NULL;

	PosPrev->next = PosNext;
	PosNext->prev = PosPrev;
}

12.链表的销毁

        我们销毁链表同样从哨兵位的后一个节点开始,等到其他节点都销毁完了后再来销毁哨兵位。

void Listdestroy(ListNode* plist)
{
	assert(plist);

	ListNode* cur = plist->next;

	while (cur != plist)
	{
		ListNode* next = cur->next;
		free(cur);

		cur = next;
	}

	free(plist);
	plist = NULL;
}

        因为我们没有传入二级指针,所以我们没法对函数外的plist实参做出更改,这就要求使用者在销毁链表后额外将plist设置为空指针来防止野指针的出现

        当然我们也可以传入二级指针然后在该接口中就完成plist的置空,但是为了接口的一致性(其他的接口都传入一级指针,总不应该就只有这个接口传入二级指针吧?),所以我们可以将这个工作交给使用者完成,就如C语言库中的free()函数。

13.尾插、尾删、头插、头删的修改

        我们在完成在特定位置前的插入和特定位置的删除后,就可以通过服用将这两个接口i将其他的插入删除接口全部替换。

//尾插

ListInsert(plist, val);

//尾删

ListErase(plist->prev);

//头插

ListInsert(plist->next, val); 

//头删

ListErase(plist->next); 


        根据上面的学习我们可以看出,带头双向循环链表的结构给我们实现相关接口带来了很大的便利,这是一个非常有用的结构,值得我们来深入学习并运用。

        以上为我个人学习过程中的认识与思考,如有错误还请指正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值