带头双向循环链表详解(图文讲解)

本文详细介绍了带头双向循环链表的结构、节点创建、初始化、插入、删除、查找以及链表的销毁等操作,并提供了相应的C语言实现和测试示例。
摘要由CSDN通过智能技术生成

一.链表的分类

先前我们已经讲了单链表,他的全名其实是不带头单向不循环链表,实际上链表有8种。 

1.带头or不带头

带头比不带头多了一个头节点head,在头节点head中不存储数据,只是当一个坐标,从d1开始才存储数据。

2.单向or双向

可以明显发现,在一个节点中,双向链表比单向多了一个指针,这个指针指向前一个结点。

3.循环or不循环

循环链表像一个圆圈一样,首尾相连。

二.实现带头双向循环链表

在上一篇文章中,我详细介绍了单向链表(不带头单向不i循环链表),现在我再来讲解一下双向链表(带头双向循环链表)

2.1 头文件(包含要实现的功能与创建一个节点)

首先来创建一个新节点,这个节点有数据域和指针域,不同的是它包含两个指针,一个指向下一个节点(next),一个指向上一个节点(prev)

typedef int SLDataType;
typedef struct SLNode {
	SLDataType data;
	struct SLNode* next;
	struct SLNode* prev;
}SLNode;

即将实现的功能

//要实现的功能!
//链表哨兵的初始化
//注意带头链表是有哨兵位的,所以插入数据之前必须要初始化一个哨兵位
SLNode* SLInit();
//哨兵位中没有数据,不需改变哨兵位,不用穿二级指针
//如果需要改变哨兵位,则传二级指针
void SLDestory(SLNode* phead);

//打印链表
void SLPrint(SLNode* phead);
//链表的头部插入和尾部插入
void SLPushFront(SLNode* phead,SLDataType x);
void SLPushBack(SLNode* phead,SLDataType x);

//链表的头删和尾删
void SLPopBack(SLNode* phead);
void SLPopFront(SLNode* pphead);

//链表的查找
SLNode* SLFind(SLNode* phead, SLDataType x);

//在pos位置之后插入数据
void SLInsert(SLNode* phead, SLNode* pos , SLDataType x);


//删除指定位置的数据
void SLErase(SLNode* phead, SLNode* pos);

//链表的销毁
void SLDestory(SLNode* phead);

2.2 实现文件

2.2.1 创建头节点

头节点是我们的带头双向循环链表的第一个节点,如何初始化他的next指针和prev指针呢?用NULL吗,显然不合理,因为它是循环的,所以目前他的prev指针与next指针指向的都是自己,基于此,我们对其进行初始化

void SLInit(LTNode* phead) {
	phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL) {
		perror("malloc fail!");
		exit(1);
	}
	(phead)->data = -1;
	(phead)->next = (phead)->prev = phead;
}

 注意一点:我们这里传过来的不是二级指针,而是一级指针!

我们只需要更改结构体中next与prev的指向,不需改变头节点plist的指向,所以我们不需要传递二级指针。

2.2.2 创建一个空节点

因为要实现的插入新节点都需要创建一个空节点,为保持代码的简洁,我们可以封装一个函数

//创建一个新节点
SLNode* BuyNewnode(int x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("set newnode");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;
	return newnode;
}

2.2.3 头部插入与尾部插入

1.尾部插入

我们只需要一步一步的更改图中的箭头指向即可。但注意要按一定顺序,否则可能出现错误,建议画图。代码如下

void SLPushBack(SLNode* phead,SLDataType x)
{
	assert(phead);
	SLNode* newnode = BuyNewnode(x);
	//不需要循环遍历找到尾部节点啦 phead->prev 就是尾节点
	newnode->next = phead;
	newnode->prev = phead->prev;
	phead->prev->next = newnode;
	phead->prev = newnode;

}

2.头部插入

 还是一个一个的更改箭头指向即可

void SLPushFront(SLNode* phead,SLDataType x )
{
	assert(phead);
	SLNode* newnode = BuyNewnode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}

2.2.4 链表的头删和尾删

1.头删

 这里我们要搞清楚头删的定义------>是指删除头节点后的第一个存有数据的节点.

void SLPopFront(SLNode* phead)
{
	assert(phead);
	//不能只有头节点
	assert(phead->next != phead);
	SLNode* pdel = phead->next;
	pdel->next->prev = phead;
	phead->next = pdel->next;
	free(pdel);
	pdel = NULL;
}

2.尾部删除

代码如下:

void SLPopBack(SLNode* phead)
{
	assert(phead);
	//链表不能为空(这里为空的意思是没有数据,只有头节点)
	assert(phead->next  != phead);
	SLNode* ptail = phead->prev;
	ptail->prev->next = phead;
	phead->prev = ptail->prev;
	free(ptail);
	ptail = NULL;
}

2.2.5 查找

注意了查找头节点没啥用,应该从第二个开始查找,所以链表是不能为空的

SLNode* SLFind(SLNode* phead, SLDataType x)
{
	//遍历链表查找
	assert(phead);
	assert(phead->next);
	//注意了查找头节点没啥用,应该从第二个开始查找,所以链表不能为空
	SLNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

这里返回了被查找数据data的指针,方便对下面对指定位置增加/删除数据的代码的检验。

2.2.6 在指定位置之后插入数据

//在pos位置之后插入数据
void SLInsert(SLNode* phead, SLNode* pos, SLDataType x)
{
	//pos只要存在,链表就不可能为空
	assert(pos);
	SLNode* pf = pos->next;
	SLNode* newnode = BuyNewnode(x);
	newnode->prev = pos;
	newnode->next = pf;
	pos->next = newnode;
	pf->prev = newnode;
}

2.2.7 删除指定位置的数据

void SLErase(SLNode* phead, SLNode* pos)
{
	assert(pos);
	assert(pos != phead);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

这里注意了,不能删除头节点,头结点处没有数据。

2.2.7 销毁链表

遍历链表进行释放,分为两种情况,只有头节点/有其他节点

void SLDestory(SLNode* phead) {
	assert(phead);
	//遍历链表,一个一个释放】
	SLNode* pcur = phead->next;
	while (pcur != phead)//直接跳过了头节点,从第二个结点开始,如果只有头节点,此时pcur == phead;
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//若是只有头节点
	free(phead);
	phead = NULL;
}

2.3 测试文件

#include "List.h"
void SLtest()
{
	//初始化头节点
	SLNode* plist = SLInit();//prev与next与plist地址相同
	SLPushBack(plist, 1);
	SLPushBack(plist, 2);
	SLPushBack(plist, 3);
	SLPushBack(plist, 4);
	SLPrint(plist);
	SLPushFront(plist, 5);
	SLPushFront(plist, 6);
	SLPrint(plist);
	/*SLPopBack(plist);
	SLPopBack(plist);*/
	SLPopFront(plist);
	SLPopFront(plist);
	SLPrint(plist);
	SLNode* pr = SLFind(plist, 4);
	/*SLInsert(plist, pr, 6);
	SLPrint(plist);*/
	/*if (pr == NULL)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找到了\n");
	}*/
	//SLErase(plist, plist);
	//SLPrint(plist); 
	void SLDestory(plist);
	SLPrint(plist); 
}
int main()
{

	SLtest();
	return 0;
}

三.全部代码🐎🐎

//头文件List.h

#pragma once
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//我们之前写的是不带头单向不循环链表,实际上链表分为8种
//1.  带头  不带头
//2.  单向  双向
//3.  循环  不循环

//今天我们来写一个带头!双向!循环链表!

//第一步--》先创建一个节点!
typedef int SLDataType;
typedef struct SLNode {
	SLDataType data;
	struct SLNode* next;
	struct SLNode* prev;
}SLNode;

//要实现的功能!
//链表哨兵的初始化
//注意带头链表是有哨兵位的,所以插入数据之前必须要初始化一个哨兵位
SLNode* SLInit();
//哨兵位中没有数据,不需改变哨兵位,不用穿二级指针
//如果需要改变哨兵位,则传二级指针
void SLDestory(SLNode* phead);

//打印链表
void SLPrint(SLNode* phead);
//链表的头部插入和尾部插入
void SLPushFront(SLNode* phead,SLDataType x);
void SLPushBack(SLNode* phead,SLDataType x);

//链表的头删和尾删
void SLPopBack(SLNode* phead);
void SLPopFront(SLNode* pphead);

//链表的查找
SLNode* SLFind(SLNode* phead, SLDataType x);

//在pos位置之后插入数据
void SLInsert(SLNode* phead, SLNode* pos , SLDataType x);


//删除指定位置的数据
void SLErase(SLNode* phead, SLNode* pos);

//链表的销毁
void SLDestory(SLNode* phead);

//实现文件List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

//创建一个新节点
SLNode* BuyNewnode(int x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("set newnode");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;
	return newnode;
}
//链表的打印
void SLPrint(SLNode* phead)
{
	SLNode* pcur = phead;
	while (pcur->next != phead)
	{
		pcur = pcur->next;
		printf("%d->", pcur->data);
	}
	printf("\n");
}

//链表的初始化与销毁
//注意带头链表是有哨兵位的,所以插入数据之前必须要初始化一个哨兵位
SLNode* SLInit()
{
	SLNode* phead = BuyNewnode(-1);
	return phead;
}

//链表的头部插入和尾部插入
void SLPushFront(SLNode* phead,SLDataType x )
{
	assert(phead);
	SLNode* newnode = BuyNewnode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}
void SLPushBack(SLNode* phead,SLDataType x)
{
	assert(phead);
	SLNode* newnode = BuyNewnode(x);
	//不需要循环遍历找到尾部节点啦 phead->prev 就是尾节点
	newnode->next = phead;
	newnode->prev = phead->prev;
	phead->prev->next = newnode;
	phead->prev = newnode;

}

//链表的头删和尾删
//无需改变头节点!

void SLPopBack(SLNode* phead)
{
	assert(phead);
	//链表不能为空(这里为空的意思是没有数据,只有头节点)
	assert(phead->next  != phead);
	SLNode* ptail = phead->prev;
	ptail->prev->next = phead;
	phead->prev = ptail->prev;
	free(ptail);
	ptail = NULL;
}
//这里我们要搞清楚头删的定义--》是指删除头节点后的第一个存有数据的节点
void SLPopFront(SLNode* phead)
{
	assert(phead);
	//不能只有头节点
	assert(phead->next != phead);
	SLNode* pdel = phead->next;
	pdel->next->prev = phead;
	phead->next = pdel->next;
	free(pdel);
	pdel = NULL;
}

//链表的查找
SLNode* SLFind(SLNode* phead, SLDataType x)
{
	//遍历链表查找
	assert(phead);
	assert(phead->next);
	//注意了查找头节点没啥用,应该从第二个开始查找,所以链表不能为空
	SLNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//在pos位置之后插入数据
void SLInsert(SLNode* phead, SLNode* pos, SLDataType x)
{
	//pos只要存在,链表就不可能为空
	assert(pos);
	SLNode* pf = pos->next;
	SLNode* newnode = BuyNewnode(x);
	newnode->prev = pos;
	newnode->next = pf;
	pos->next = newnode;
	pf->prev = newnode;
}


//删除指定位置的数据
void SLErase(SLNode* phead, SLNode* pos)
{
	assert(pos);
	assert(pos != phead);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

//链表的销毁
void SLDestory(SLNode* phead) {
	assert(phead);
	//遍历链表,一个一个释放】
	SLNode* pcur = phead->next;
	while (pcur != phead)//直接跳过了头节点,从第二个结点开始,如果只有头节点,此时pcur == phead;
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//若是只有头节点
	free(phead);
	phead = NULL;
}

//测试文件Test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "List.h"
void SLtest()
{
	//初始化头节点
	SLNode* plist = SLInit();//prev与next与plist地址相同
	SLPushBack(plist, 1);
	SLPushBack(plist, 2);
	SLPushBack(plist, 3);
	SLPushBack(plist, 4);
	SLPrint(plist);
	SLPushFront(plist, 5);
	SLPushFront(plist, 6);
	SLPrint(plist);
	/*SLPopBack(plist);
	SLPopBack(plist);*/
	SLPopFront(plist);
	SLPopFront(plist);
	SLPrint(plist);
	SLNode* pr = SLFind(plist, 4);
	/*SLInsert(plist, pr, 6);
	SLPrint(plist);*/
	/*if (pr == NULL)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找到了\n");
	}*/
	//SLErase(plist, plist);
	//SLPrint(plist); 
	void SLDestory(plist);
	SLPrint(plist); 
}
int main()
{

	SLtest();
	return 0;
}

四。结束啦!一起进步,越来越好!

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值