【数据结构】-- 带头双向循环链表

【数据结构】-- 带头双向循环链表_第1张图片

*

博客主页:云曦
系列专栏:数据结构

吾生也有涯,而知也无涯
感谢大家点赞 关注评论

文章目录

  • 前言
  • 一、什么是双链表
    • 1.1 双链表的介绍
    • 1.2 双链表的结构
  • 二、双链表的实现
    • 2.1 双链表获取节点
    • 2.2 双链表的初始化
    • 2.3 双链表的打印
    • 2.4 双链表的尾插
    • 2.5 双链表的尾删
    • 2.6 双链表的头插
    • 2.7 双链表的头删
    • 2.8 双链表查找
    • 2.9 双链表在pos位置之前插入节点
    • 2.10 双链表删除pos位置的节点
    • 2.11 双链表的销毁
  • 三、双链表头插头删尾插尾删的优化
    • 3.1 头插优化
    • 3.2 尾插优化
    • 3.3 头删优化
    • 3.4 尾删优化
  • 四、顺序表和链表的区别
  • 五、完整代码

前言

在上期我们学了单链表,但单链表有个缺点就是尾插尾删不方便,所以这期我们要学习到一个非常完美的链表:带头双向循环链表。至于有多完美大家看完就知道了。

一、什么是双链表

1.1 双链表的介绍

双链表含义其实就是每个节点可以找到前后的节点。而双链表也分为带头或不带头、循环或非循环,本章用的是带头且循环的双链表,称为:带头双向循环链表
结构:【数据结构】-- 带头双向循环链表_第2张图片注意:
1. 链式结果只是在逻辑上是连续的,但在物理上就未必是连续的了。
2. 链表的节点是从堆上申请的空间。
3.从堆上申请的空间,可能是连续的也有可能不连续。

1.2 双链表的结构

typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* prev;//上一个节点的地址
	struct ListNode* next;//下一个节点的地址
	LTDataType data;//存储数据
}LTNode;

此链表是带头+双向+循环的链表

二、双链表的实现

2.1 双链表获取节点

想要使用链表要先创建获取节点的接口

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		//申请空间失败则打印错误信息并退出程序
		perror("malloc");
		exit(-1);
	}
	//申请成功则给节点初始化一下
	//避免一些不必要的麻烦
	newnode->data = x;
	newnode = NULL;
	newnode = NULL;

	return newnode;
}

2.2 双链表的初始化

因为是带头的双链表,所以要初始化一下

LTNode* LTInit()
{
	//创建哨兵卫(头)
	LTNode* head = BuyLTNode(-1);
	//让头的前后指针指向自己,形成循环
	head->next = head;
	head->prev = head;

	return head;
}

这里要注意:初始化接口也可以传二级来初始化,但传二级指针后其他接口都是一级指针,显得有点不统一。(但传二级指针也是可以的,这里只是给大家一个建议用返回值来返回初始化链表)

2.3 双链表的打印

有了链表当然需要一个用来打印链表的接口了

void LTPrint(const LTNode* phead)
{
	//检查是否为空指针
	assert(phead);
	const LTNode* cur = phead->next;
	printf("head<=>");
	//遍历链表
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	pritnf("\n");
}

注意:
因为之前实现单链表的时候,是无头的,但这里是带头的双向链表,所以遍历双链表的条件是判断是否等于头

2.4 双链表的尾插

节点的获取、初始化链表和打印的接口都有了,接下来就是实现尾插的接口了。

现在是双链表且带头,实现起来就简单多了,实现思路:
先让尾节点的next指向新节点,新节点的prev指向尾节点,最后让新节点的next指向头,让头的prev指向新节点。
【数据结构】-- 带头双向循环链表_第3张图片

void LTpushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//断言
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

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

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

【数据结构】-- 带头双向循环链表_第4张图片

2.5 双链表的尾删

有了插入数据的接口,自然要有删除的接口。

实现起来也很简单:
先让头指向尾节点的前一个,让尾节点前一个的next指向头,最后释放尾节点
【数据结构】-- 带头双向循环链表_第5张图片

void LTPopBack(LTNode* phead)
{
	assert(phead);//断言
	//检查是否只有哨兵卫了
	assert(phead != phead->next);
	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;

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

【数据结构】-- 带头双向循环链表_第6张图片

当链表只剩下哨兵卫还在删时:
【数据结构】-- 带头双向循环链表_第7张图片

2.6 双链表的头插

头插的实现思路也很简单:
将front->prev指向newnode,然后让newnode->next指向front,最后让phead->next指向newnode,让newnode->prev指向phead。
【数据结构】-- 带头双向循环链表_第8张图片

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//断言
	LTNode* front = phead->next;
	LTNode* newnode = BuyLTNode(x);
	
	newnode->next = front;
	front->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

【数据结构】-- 带头双向循环链表_第9张图片

2.7 双链表的头删

头删也简单:定义front和frontNext两个指针,将phead->next指向frontNext,然后将frontNext->prev指向头,最后释放掉front。

【数据结构】-- 带头双向循环链表_第10张图片

void LTPopFront(LTNode* phead)
{
	assert(phead);//断言
	//检查是否只有哨兵卫了
	assert(phead != phead->next);
	LTNode* front = phead->next;
	LTNode* frontNext = front->next;

	phead->next = frontNext;
	frontNext->prev = phead;
	free(front);
	front = NULL;
}

【数据结构】-- 带头双向循环链表_第11张图片

2.8 双链表查找

链表查找直接遍历一遍链表即可,找到了返回这个节点,找不到返回NULL。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);//断言
	LTNode* cur = phead->next;
	//因为是带头循环链表
	//所以结束条件为等于头
	while (cur != phead)
	{

		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

2.9 双链表在pos位置之前插入节点

实现思路:定义一个posprev的指针,将newnode->next指向pos,再让pos->prev指向newnode,然后将posprev->next指向newnode,最后让newnode->prev指向posprev。
【数据结构】-- 带头双向循环链表_第12张图片

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//断言
	LTNode* posprev = pos->prev;
	LTNode* newnode = BuyLTNode(x);
	
	newnode->next = pos;
	pos->prev = newnode;
	posprev->next = newnode;
	newnode->prev = posprev;
}

【数据结构】-- 带头双向循环链表_第13张图片

2.10 双链表删除pos位置的节点

实现思路:定义在pos前后节点的两个指针,将posprev->next指向posnext,然后让posnext->prev指向posprev,最后释放掉pos。
【数据结构】-- 带头双向循环链表_第14张图片

void LTErase(LTNode* pos)
{
	//检查pos是否为空指针
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;

	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;
}

【数据结构】-- 带头双向循环链表_第15张图片

2.11 双链表的销毁

注意:这里的销毁指的是:把空间还给操作系统了。

思路:复用一下LTFind,然后把if删了,再创建一个next指针指向cur->next,再释放cur,把next的地址给cur,遍历完后释放哨兵卫即可。

void LTDestroy(LTNode* phead)
{
	//检查是否为空指针
	assert(phead);
	LTNode* cur = phead->next;
	//遍历销毁链表
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

三、双链表头插头删尾插尾删的优化

3.1 头插优化

复用LTInsert接口,将头的下一个节点和x传入即可。

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//断言
	//复用在pos位置前面插入节点的接口
	LTInsert(phead->next, x);
}

【数据结构】-- 带头双向循环链表_第16张图片

3.2 尾插优化

复用一下LTInsert接口即可,参数传phead->prev和x,逻辑上是在头前面插入,但在物理上是在尾部插入,因为头上一个的节点就是尾节点。

void LTpushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//断言
	//复用在pos位置前面插入节点的接口
	LTInsert(phead->prev, x);
}

【数据结构】-- 带头双向循环链表_第17张图片

3.3 头删优化

复用LTErase接口,把头的下一个节点传入即可

void LTPopFront(LTNode* phead)
{
	assert(phead);//断言
	//检查是否只有哨兵卫了
	assert(phead != phead->next);
	//复用删除pos位置节点的接口
	LTErase(phead->next);
}

【数据结构】-- 带头双向循环链表_第18张图片

3.4 尾删优化

复用LTErase接口,把头上一个节点传入即可

void LTPopBack(LTNode* phead)
{
	assert(phead);//断言
	//检查是否只有哨兵卫了
	assert(phead != phead->next);
	//复用删除pos位置节点的接口
	LTErase(phead->prev);
}

【数据结构】-- 带头双向循环链表_第19张图片

四、顺序表和链表的区别

不同点 顺序表 链表
存储空间上 物理一定连续 逻辑上连续,物理上不一定连续
随机访问 支持:时间复杂度为O(1) 不支持:时间复杂度为O(N)
任意位置插入删除元素 可以搬移元素,但效率低,复杂度为O(N) 只需要修改指针的指向
插入 动态顺序表空间不够,需要扩容 没有容量概念
应用场景 元素高效存储和频繁访问 任意位置插入和删除频繁
缓存利用率

注意:这里是拿最优秀的带头双向循环链表来比较的

五、完整代码

List.h

#pragma once
#include
#include
#include

typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* prev;//上一个节点的地址
	struct ListNode* next;//下一个节点的地址
	LTDataType data;//存储数据
}LTNode;

//获取新节点
LTNode* BuyLTNode(LTDataType x);
//初始化双链表
LTNode* LTInit();
//打印双链表
void LTPrint(const 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);
//销毁双链表
void LTDestroy(LTNode* phead);

List.c

#include "Slist.h"

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		//申请空间失败则打印错误信息并退出程序
		perror("malloc");
		exit(-1);
	}
	//申请成功则给节点初始化一下
	//避免一些不必要的麻烦
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

LTNode* LTInit()
{
	//创建哨兵卫(头)
	LTNode* head = BuyLTNode(-1);
	//让头的前后指针指向自己,形成循环
	head->next = head;
	head->prev = head;

	return head;
}

void LTPrint(const LTNode* phead)
{
	//检查是否为空指针
	assert(phead);
	const LTNode* cur = phead->next;
	printf("head<=>");
	//遍历链表
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

void LTpushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//断言
	//复用在pos位置前面插入节点的接口
	LTInsert(phead->prev, x);

	/*LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

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

	newnode->next = phead;
	phead->prev = newnode;*/
}

void LTPopBack(LTNode* phead)
{
	assert(phead);//断言
	//检查是否只有哨兵卫了
	assert(phead != phead->next);
	//复用删除pos位置节点的接口
	LTErase(phead->prev);
	/*LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;

	phead->prev = tailprev;
	tailprev->next = phead;
	free(tail);
	tail = NULL;*/
}

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);//断言
	//复用在pos位置前面插入节点的接口
	LTInsert(phead->next, x);
	/*LTNode* front = phead->next;
	LTNode* newnode = BuyLTNode(x);
	
	newnode->next = front;
	front->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;*/
}

void LTPopFront(LTNode* phead)
{
	assert(phead);//断言
	//检查是否只有哨兵卫了
	assert(phead != phead->next);
	//复用删除pos位置节点的接口
	LTErase(phead->next);
	/*LTNode* front = phead->next;
	LTNode* frontNext = front->next;

	phead->next = frontNext;
	frontNext->prev = phead;
	free(front);
	front = NULL;*/
}

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* cur = phead->next;
	//因为是带头循环链表
	//所以结束条件为等于头
	while (cur != phead)
	{

		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//断言
	LTNode* posprev = pos->prev;
	LTNode* newnode = BuyLTNode(x);
	
	newnode->next = pos;
	pos->prev = newnode;
	posprev->next = newnode;
	newnode->prev = posprev;
}

void LTErase(LTNode* pos)
{
	//检查pos是否为空指针
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;

	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;
}

void LTDestroy(LTNode* phead)
{
	//检查是否为空指针
	assert(phead);
	LTNode* cur = phead->next;
	//遍历销毁链表
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

你可能感兴趣的:(数据结构,数据结构,链表)