C语言——详解带头双向循环链表 增删查改

2023-04-25 18:36:21

假设存储在链表中的数据是int类型的,重命名为LTDataType

typedef int SLDataType;

定义一个结构体,每个结构体是一个结点,在这个结点中包括指向上一个节点的指针,指向下一个结点的指针以及有效数据,将结构体重命名为List

typedef struct List
{
	struct List* prev;
	struct List* next;
	SLDataType data;
}List;

在头文件中声明要实现功能的函数

//声明函数

//初始化双向链表
List* InitList();

//销毁双向链表
void DestoryList(List* phead);

//创建结点
List* CreateNode(SLDataType x);

//打印双向链表
void PrintList(List* phead);

//头插
void ListPushFront(List* phead, SLDataType x);

//尾插
void ListPushBack(List* phead, SLDataType x);

//头删
void ListPopFront(List* phead);

//尾删
void ListPopBack(List* phead);

//查找结点
List* ListFind(List* phead, SLDataType x);

//插入中间
void ListInsert(List* pos, SLDataType x);

//删除中间结点
void ListErase(List* pos);

//计算有效结点数目
int ListSize(List* phead);

//判断双向链表是否为空
bool ListEmpty(List* phead);

1.创建新结点

//创建结点
List* CreateNode(SLDataType x)
{
	List* newnode = (List*)malloc(sizeof(List));
	
	//初始化结点
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

创建完结点后初始化它,将x赋给它的data,因为返回值是结点的指针,因此在这里不需要传二级指针,直接将创建的结点返回即可。 

2.初始化双向链表

//初始化双向链表
List* InitList()
{
	List* phead = CreateNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

初始化双向链表,因为是带头的,所以先创建头结点,调用CreateNode函数,头结点的prev和next都指向自己,初始化就完成了。

 3.销毁双向链表

//销毁双向链表
void DestoryList(List* phead)
{
	assert(phead);

	List* cur = phead->next;
	while (cur != phead)
	{
		List* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
	phead = NULL;
}

要销毁双向链表,需要将每个结点都free掉,最后还要将哨兵位的结点free掉,并置为空,在free的过程中要创建next记住下一个结点,因为free掉当前节点后就找不到下一个结点了,因此要记住它

4.打印双向链表有效数据

//打印双向链表
void PrintList(List* phead)
{
	assert(phead);

	List* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

 这个没什么好说的,结束条件是当cur等于phead

5.头插

//头插
void ListPushFront(List* phead, SLDataType x)
{
	assert(phead);

	List* first = phead->next;
	List* newnode = CreateNode(x);

	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;

	//ListInsert(phead->next, x);
}

双向循环链表的一个很大的好处就是他不会指向空,所以不论除了头结点外你是否还有结点,他都可以连起来

6.尾插

//尾插
void ListPushBack(List* phead, SLDataType x)
{
	assert(phead);

	List* tail = phead->prev;
	List* newnode = CreateNode(x);

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;

	//ListInsert(phead, x);
}

 跟头插一样,除了头结点外,不管你有没有结点,他都能连起来。没有结点的话就跟头插是一样的

 7.头删

//头删
void ListPopFront(List* phead)
{
	assert(phead);
	assert(phead->next != phead);

	List* first = phead->next;
	List* second = first->next;

	phead->next = second;
	second->prev = phead;

	free(first);
	first = NULL;

	//ListErase(phead->next);
}

在这里要断言必须起码有一个有效节点,将第一个结点定义为first,第二个节点定义为second,将phead和second连起来,free掉first就行了

8.尾删

//尾删
void ListPopBack(List* phead)
{
	assert(phead);
	assert(phead->next != NULL);

	List* tail = phead->prev;
	List* prev = tail->prev;

	prev->next = phead;
	phead->prev = prev;

	free(tail);
	tail = NULL;

	//ListErase(phead->prev);
}

 跟头删一样,也要断言必须起码有一个结点,将最后一个节点定义为tail,tail的前一个结点定义为prev,将phead和prev连起来,再将tail给free掉就行了,如果只有一个结点的话也是成立的

 

9.查找结点

//查找结点
List* ListFind(List* phead, SLDataType x)
{
	assert(phead);

	List* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

 遍历链表,从phead的next开始,如果找到了就返回那个结点的地址,找不到就返回空指针,结束条件是当cur==phead

10.插入中间

//插入中间
void ListInsert(List* pos, SLDataType x)
{
	assert(pos);

	List* prev = pos->prev;
	List* newnode = CreateNode(x);

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

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

 通过ListFind函数找到要插入的结点,pos前面的结点定义为prev,将prev和newnode连起来,再将newnode和pos连起来jiuxing

11.删除中间结点

//删除中间结点
void ListErase(List* pos)
{
	assert(pos);

	List* prev = pos->prev;
	List* next = pos->next;
	prev->next = next;
	next->prev = prev;

	free(pos);
	pos = NULL;
}

通过ListFind函数找到要删除的结点pos,pos前的结点定义为prev,后一个结点定义为next,prev和next连起来,再将pos给free掉即可

 

12.计算有效结点数目

//计算有效结点数目
int ListSize(List* phead)
{
	assert(phead);

	List* cur = phead->next;
	int count = 0;
	while (cur != phead)
	{
		count++;
		cur = cur->next;
	}

	return count;
}

 定义count变量用来记录,遍历链表,在返回count

 13.判断链表是否为空

//判断双向链表是否为空
bool ListEmpty(List* phead)
{
	assert(phead);

	return phead->next == phead;
}

如果头结点的next还是头结点的话,就表示除了头结点,没有其他结点,也即是说链表是空,返回true,反之就返回false 

注意:在头插尾插头删尾删中分别可以调用ListInsert、ListErase函数,头插就将phead的next地址传给ListInsert函数,尾插将phead传给ListInsert,头删将phead的next传给ListErase,尾删就将phead的prev传给ListErase,效果是一样的,在将来面试如果面试官让你在短时间内写出增删查改等基本操作,可以使用这样的方法节省时间。

下面是一些测试用例

void test1()
{
	List* head = InitList();

	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	PrintList(head);

	ListPushFront(head, 0);
	ListPushFront(head, -1);
	PrintList(head);

	ListPopFront(head);
	PrintList(head);

	ListPopBack(head);
	ListPopBack(head);
	PrintList(head);

	List* pos = ListFind(head, 2);
	if (pos)
	{
		ListInsert(pos, 20);
	}
	PrintList(head);

	ListErase(pos);
	PrintList(head);

	printf("%d\n", ListSize(head));

	DestoryList(head);
}

void test2()
{
	List* head = InitList();

	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	PrintList(head);

	ListPushFront(head, 0);
	ListPushFront(head, -1);
	PrintList(head);

	List* pos = ListFind(head, 3);
	if (pos)
	{
		ListInsert(pos, 30);
	}
	PrintList(head);

	ListErase(pos);
	PrintList(head);

	printf("%d\n", ListSize(head));

	DestoryList(head);
}

int main()
{
	test1();
	test2();
	return 0;
}

 

将他们对照着理解效果会更好

以上就是本次带头双向链表的一些基本操作,如果对你有用的话就点个赞吧...... 

 

 

  • 作者:忘れな草と永遠の加藤惠
  • 原文链接:https://blog.csdn.net/qq_62476499/article/details/122386926
    更新时间:2023-04-25 18:36:21