带头双向循环链表的增删查改

带头双向循环链表的增删查改

简介

首先我们来看一下带头双向循环链表的结构示意图,在实际内存中并非是这样的结构,画图是为了我们能更好的理解链表

在这里插入图片描述

带头双向循环链表的结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。虽然这个结构虽然复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了 。

结构体的创建和函数的声明

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

// 创建一个新结点
LTNode* BuyListNode(LTDataType x);

// 初始化链表
LTNode* ListInit();

// 打印链表
void LTPrint(LTNode* phead);

// 尾插
void LTPushBack(LTNode* phead, LTDataType x);

// 尾删
void LTPopBack(LTNode* phead);

// 头插
void LTPushFront(LTNode* phead, LTDataType x);

// 头删
void LTPopFront(LTNode* phead);

// 查找
LTNode* LTFind(LTNode* phead, LTDataType x);

// 在POS之前插入数据
void LTInsert(LTNode* pos, LTDataType x);

// 删除pos位置的数据
void LTErase(LTNode* pos);

// 链表判空
bool LTEmpty(LTNode* phead);

// 链表大小
size_t LTSize(LTNode* phead);

// 链表销毁
void LTDestroy(LTNode* phead);

函数功能的实现

创建一个新结点

创建新结点在初始化和插入数据时都需要使用,选择使用malloc开辟空间是为了在程序运行整个过程中链表都不会因为出了函数作用域而销毁。

这里我们使用LTDataType代替int是方便后续代码的修改,只需在头文件定义中修改数据类型就可实现不同类型的链表。

LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	node->data = x;
	node->next = NULL;
	node->prev = NULL;

	return node;
}

初始化链表

初始化时与单链表稍有不同,初始化链表为空时需要创建一个哨兵位的头结点phead

头结点的prev要指向尾结点,尾结点的next要指向头结点phead,这样才会形成循环。

但我们在初始化的时候,链表中是没有数据的,那么phead头结点的prevnext应该如何初始化呢?

因此,在初始化链表时,我们需要将头结点phead中的prevnext都指向自己,这样就会形成循环。

在这里插入图片描述

LTNode* ListInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

打印链表

打印链表时从头结点的下一个结点phead->next开始向后遍历并打印,直到遍历到头结点处(phead->next == phead)时便停止遍历和打印。

void LTPrint(LTNode* phead)
{
	assert(phead);

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

尾插

尾插数据时,首先要寻找链表的尾结点tail,不论链表是否为空,我们都能通过phead->prev找到尾结点,然后将新结点插入尾结点之后即可。

在这里插入图片描述

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	// 创建节点
	LTNode* newnode = BuyListNode(x);
	// 寻找尾节点
	LTNode* tail = phead->prev;
	// 插入数据
	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;
}

尾删

尾删时,需要找到两个结点,尾结点tail尾结点的前一个结点tailPrev,将尾结点删除,再将phead->prev指向tailPrev,重新构成循环。

因为每一个结点都是malloc开辟的,在删除结点时还需要把它释放掉。

删除结点时,需要判断是否存在结点。

在这里插入图片描述

void LTPopBack(LTNode* phead)
{
	assert(phead);
	// 判断是否只有一个头结点
	assert(phead->next != phead);

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);
}

头插

创建一个新结点newnode,把它插入phead->next处。

在这里插入图片描述

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = BuyListNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

头删

头删和尾删类似,需要判断链表是否为空,把要删的结点phead->next和要删除结点的下一个结点phead->next->next找到,将头结点指向删除结点的后一个结点,记得free掉删除的结点,防止内存泄露。

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* cur = phead->next;
	LTNode* next = cur->next;
	free(cur);
	phead->next = next;
	next->prev = phead;
}

查找

遍历链表中的数据,如果匹配就返回当前结点的地址,如果没有找到返回NULL

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

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

在pos位置之前插入数据

在pos位置之前插入数据,找到pos位置之前的结点,创建新的结点插入pos之前。

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyListNode(x);
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

我们之前写的头插和尾插函数,可以复用LTInsert()函数。

头插就相当于在头结点的下一个结点phead->next前插入一个数据。

LTInsert(phead->next, x);

尾插就相当于在头结点phead前插入一个数据。

LTInsert(phead,x);

删除pos位置的数据

删除pos位置的数据时,先要找到pos位置前后两个结点pos->prevpos->next,前后两个结点链接后free掉pos位置的结点。

void LTErase(LTNode* pos)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	free(pos);
	prev->next = next;
	next->prev = prev;
}

同样头删和尾删也可以复用LTErase()函数。

头删就相当于删除头结点的下一个结点phead->next

LTErase(phead->next);

尾删就就相当于删除头结点的前一个结点phead->prev

LTErase(phead->prev);

链表判空

如果phead->next == phead,那就说明链表中没有数据。

bool LTEmpty(LTNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

链表大小

遍历链表,统计结点个数,并返回。

size_t LTSize(LTNode* phead)
{
	assert(phead);

	size_t size = 0;
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

链表销毁

遍历销毁链表,这里要注意提前记录删除当前结点的下一个结点,如果直接释放,会导致无法找到后续结点,造成内存泄漏。

void LTDestroy(LTNode* phead)
{
	assert(phead);

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

演示案例

void TestList1()
{
	LTNode* phead = ListInit();
	// 尾插
	LTPushBack(phead, 1);
	LTPushBack(phead, 2);
	LTPushBack(phead, 3);
	LTPushBack(phead, 4);
	LTPushBack(phead, 5);
	LTPrint(phead);
	
	// 尾删
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);
	LTPopBack(phead);
	LTPrint(phead);

	// 头插
	LTPushFront(phead, 1);
	LTPushFront(phead, 2);
	LTPushFront(phead, 3);
	LTPushFront(phead, 4);
	LTPushFront(phead, 5);
	LTPrint(phead);

	// 头删
	LTPopFront(phead);
	LTPrint(phead);
	LTPopFront(phead);
	LTPrint(phead);
}
int main()
{
	TestList1();

	return 0;
}

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值