带头双向循环链表

一:结构体的创建和函数的声明(List.h)

请添加图片描述

  • 带头双向循环链表的结构如图,看起来是不是有亿点复杂,但是使用起来却十分方便
  • 可以看到,它有一个不存储数据的头节点,每个节点中有三个值,一个是prev指针,一个是数据域data,和一个next指针改变
  • 其中头结点的prev指向尾节点,尾结点的next指向头节点,形成了循环
  • 其余节点的prev指向上一个节点,next指向下一个节点,构成了双向
  • 所以定义结构体的时候也需要有这三个变量
  • 注意data的类型不要直接写死成int或者其它类型,先将它的类型重定义为ListDataType
  • 之后要想改变data的类型,只需改变typedef后的类型即可
  • 这些操作都放在List.h中完成,便于其它文件调用
  • 剩下就是进行函数的声明,我们需要的函数有初始化,打印,头插头删尾插尾删,查找,任意位置插入删除,销毁这几个函数。
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int ListDataType;
typedef struct ListNode
{
	ListDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;
LTNode* ListInit();
void ListPushBack(LTNode* phead, ListDataType x);
void ListPrint(LTNode* phead);
void ListPopBack(LTNode* phead);
void ListPushFront(LTNode* phead, ListDataType x);
void ListPopFront(LTNode* phead);
LTNode* ListFind(LTNode* phead, ListDataType x);
void ListInsert(LTNode* pos, ListDataType x);
void ListErase(LTNode* pos);
void ListDestroy(LTNode* phead);

二:函数的具体实现(List.c)

1. ListInit(初始化链表)

请添加图片描述

  • 由于刚开始只有一个节点,也就是头结点,又要构成双向循环,所以头结点的prev和next都指向的是头结点自己
  • 这里初始化没有传二级指针,而是采取了返回头结点的方式。所以需要先malloc申请一个头结点phead
  • 然后将phead的prev和next都指向phead自己,初始化就大功告成了。
  • 代码如下:
LTNode* ListInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	phead->prev = phead;
	phead->next = phead;
	return phead;
}

2. ListPrint(打印链表)

  • 首先先断言链表不为空,为空就不用打印了
  • 然后就是利用cur指针从头节点的下一个节点遍历链表(头结点不存储有效数据)节点打印数据域data,问题是终止条件是什么
  • 单链表时在cur为为NULL时就到尾了,可是带头双向链表没有指向NULL的节点
  • 其实也很简单,当cur再次为头结点phead时说明已经遍历链表一遍了,这就是终止条件
  • 我这里是为了再每个节点后打印->方便观看,所以是让cur的next不为phead为终止条件,最后再单独打印最后一个节点,如果只有头结点就不打印
  • 代码如下:
void ListPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur->next != phead)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	//如果只有头结点,不打印
	if (phead->next != phead)
	{
		printf("%d\n", cur->data);
	}
}

3.BuyListNode(申请新节点)

  • 此函数为辅助函数,是为了后面头插,尾插和任意位置插入时申请头结点,减少代码的重复
  • 实现起来也十分容易,申请一个节点将其进行初始化后返回
  • 代码如下:
LTNode* BuyListNode(ListDataType x)
{
	LTNode* NewNode = (LTNode*)malloc(sizeof(LTNode));
	NewNode->data = x;
	NewNode->next = NULL;
	NewNode->prev = NULL;
	return NewNode;
}

4. ListPushBack (尾插)

  • 比起单链表的尾插,带头双向循环链表进行尾插就十分容易,因为它不用遍历寻找尾结点,头结点phead的prev指向的就是尾结点
  • 先断言防止链表为初始化而为空链表
  • 再使用BuyListNode函数申请一个新节点NewNode,通过头节点的prev找到尾结点
  • 将尾结点tail的next链接上NewNode,再将NewNode的prev指向tail
  • 此时NewNode为新的尾结点,将其next指向头结点phead,再将phead的prev指向新的尾结点NewNode,尾插就完成了
  • 当然之后完成ListInsert以后就可以直接调用它完成尾插
void ListPushBack(LTNode* phead, ListDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* NewNode = BuyListNode(x);
	//将新节点插入
	tail->next = NewNode;
	NewNode->prev = tail;
	NewNode->next = phead;
	phead->prev = NewNode;
	//ListInsert(phead,x);
}

5. ListPopBack(尾删)

  • 进行尾删时我们不仅要断言链表不为空,并且要断言链表不能只有头节点,因为我们不希望将头结点也删除
  • 之后通过phead的prev找到尾结点tail,创建变量tailprev指向tail的上一个节点
  • 因为tail节点要被释放,所以tailprev是尾删后链表的新尾节点
  • 将tailprev的next指向头结点phead,phead的prev指向tailprev
  • 最后释放tail节点,就完成了尾删
  • 完成ListErase后可以直接调用完成尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTNode* tail = phead->prev;
	tail->prev->next = phead;
	phead->prev = tail->prev;
	free(tail);
	tail = NULL;
	//ListErase(phead->prev);
}

6. ListPushFront(头插)

  • 这里说是头插但并不是在头结点phead前面插入,无论如何phead都是头节点,它就是个哨兵,起的站岗作用,所以头插其实是在头结点phead后面进行插入
  • 创建变量next指向第二个节点就是头结点的下一个节点,之后在next前面进行插入新节点
  • 之后的工作就是将phead的next指向NewNode,NewNode的prev指向phead
  • NewNode的next指向next,next的prev指向NewNode完成双向链接
  • 完成ListInsert以后就可以直接调用它完成头插
void ListPushFront(LTNode* phead, ListDataType x)
{
	assert(phead);
	LTNode* NewNode = BuyListNode(x);
	LTNode* next = phead->next;
	phead->next = NewNode;
	NewNode->prev = phead;
	NewNode->next = next;
	next->prev = NewNode;
	//ListInsert(phead->next, x);
}

7. ListPopFront(头删)

  • 进行头删时我们不仅要断言链表不为空,并且要断言链表不能只有头节点,因为我们不希望将头结点也删除
  • 并且头删删除的也并不是头节点,是头节点的下一个节点
  • 之后通过phead的next找到要删除的结点next,创建变量Nextnext指向next的下一个节点
  • 然后将Nextnext和头节点phead进行链接,具体实现不赘述了
  • 最后释放next节点,就完成了头删
  • 完成ListErase后可以直接调用完成头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTNode* next = phead->next;
	LTNode* nestNest = next->next;
	phead->next = nestNest;
	nestNest->prev = phead;
	free(next);
	next = NULL;
	//ListErase(phead->next);
}

8. ListFind(查找链表元素)

  • 这个函数很少单独使用,更多的是为了辅助ListInsert,ListErase这两个函数,通过ListFind找到要插入删除的节点,然后将其地址传给ListInsert,ListErase进行插入或删除
  • 实现也很简单,使用cur指针遍历链表,对比每一个节点data的值和x的值,x代表使用者想要查找的值
  • 如果相等就返回节点地址,如果找不到就返回NULL
  • 通过ListFind还可以实现链表元素的修改
  • 请添加图片描述
LTNode* ListFind(LTNode* phead, ListDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

9. ListInsert(任意位置前插入节点)

  • pos参数一般是基于ListFind查找而来,故这两个函数经常搭配使用
  • 之后创建变量posprev指向pos的上一个节点,申请新节点将其prev指向posprev,将其next指向pos
  • posprev的next指向新节点NewNode,pos的prev指向NewNode
  • 这样pos位置之前的插入就完成了,并且可以替代头插尾插
    请添加图片描述
//在pos之前插入
void ListInsert(LTNode* pos, ListDataType x)
{
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* NewNode = BuyListNode(x);
	NewNode->prev = posprev;
	posprev->next = NewNode;
	NewNode->next = pos;
	pos->prev = NewNode;
}

10. ListErase(删除任意位置节点)

  • pos参数一般是基于ListFind查找而来,故这两个函数经常搭配使用
  • 之后创建变量posprev指向pos的上一个节点,创建变量posnext指向pos的下一个节点
  • 将posprev与posnext链接,释放pos,就完成pos位置的删除了,并且可以替代掉尾删头删
  • 请添加图片描述
void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;
	posPrev->next = posNext;
	posNext->prev = posPrev;
	pos = NULL;
}

11. ListDestroy(销毁链表)

  • 最后是销毁链表,这里有个很容易犯的错误就是直接free(phead),这样只是释放了头节点,链表的其它节点并未释放,会造成内存泄露。
  • 所以我们需要通过cur遍历链表所有节点一个一个进行释放,并且防止释放了cur找不到cur的下一个节点,需要先创建临时变量进行保存
  • 最后将phead置为空指针代表链表为空,即销毁完成。
void ListDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	phead = NULL;
}
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dhdw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值