【数据结构与算法篇】双向带头循环链表的C++实现

前言

链表按照是否带头, 是否循环 , 单向双向共有八种组合

  • 不带头单向不循环链表
  • 不带头单向循环链表
  • 不带头双向不循环链表
  • 不带头双向循环链表
  • 不带头单向不循环链表
  • 不带头单向循环链表
  • 不带头双向不循环链表
  • 不带头双向循环链表

本文将对带头双向循环链表的概念, 实现 , 增删查改等进行介绍

一 . 结构体的定义

  • 我们知道, 链表由一个或多个节点组成 。
  • 每个节点又分为数据域和指针域
  • 循环链表的指针域需要两个指针, 一个用于指向上一个节点, 另一个用于指向下一个节点
  • 是否带头指的是 , 该链表是否有 哨兵头节点
  • 哨兵头节点不存储具体的数据, 只是起到一个监视的作用

根据以上信息来对节点进行定义

typedef int LTDataType;      // 该示例链表所存储数据为 int 类型
typedef struct ListNode
{
	LTDataType data;         // 数据域, 变量data 存放数据
	ListNode* prev;          // 指针域, 存放上一节点的地址
	ListNode* next;          // 指针域, 存放下一节点的地址 
}ListNode;     // 结构体别名

二 . 链表的初始化

在我们定义结构体之后, 要对链表进行初始化。

  • 实际上是声明一个头指针, 以及构建一个哨兵头节点
  • 头指针指向该哨兵头节点
// 根据指定值, 创建节点
ListNode* BuyLTNode(LTDataType val)
{
	ListNode* newNode = new ListNode;
	if (!newNode)
	{
		perror("new is failed!");
		exit(-1);
	}
	newNode->data = val;
	newNode->next = NULL;
	newNode->prev = NULL;
	return newNode;
}


// 链表初始化 , 设置哨兵头节点
ListNode* LTInit()
{
	ListNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

ListNode* phead = LTInit();  // 使得头指针指向哨兵头节点

三 . 链表的增删改查

1 . 向链表中插入新节点

1) 头插法

// 链表头插
void LTPushFront(ListNode* phead, LTDataType val)
{
	if (!phead)     // 判断头指针是否为空, 即链表是否被初始化
	{
		perror("phead is NULL");
		exit(-1);
	}
	ListNode* newNode = BuyLTNode(val);
	ListNode* cur = phead->next;
	phead->next = newNode;
	newNode->prev = phead;

	newNode->next = cur;
	cur->prev = newNode;
}

2) 尾插法

// 链表尾插
void LTPushBack(ListNode* phead, LTDataType val)
{
	if (!phead)      // 判断头指针是否为空, 即链表是否被初始化
	{
		perror("phead is NULL");
		exit(-1);
	}
	ListNode* newNode = BuyLTNode(val);
	ListNode* tail = phead->prev;
	tail->next = newNode;
	newNode->prev = tail;

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

3) 指定位置前插入

// 链表 指定位置插入
void LTInsert(ListNode* pos, LTDataType val)
{    // 这里的pos是指向节点的指针, 我们要插入的节点, 是插入到pos指向的节点之前的位置。
     // 在这里可以增加一步实现, 判断pos是否等于phead , 这就需要传入第二个参数 phead
	if (!pos)
	{
		perror("pos is NULL");
		exit(-1);
	}
	ListNode* prev = pos->prev;
	ListNode* next = pos;     // 这一步也可以不创建临时变量, 直接使用pos就可以 , 不会修改pos指向  创建临时变量只是为了名字上统一
	ListNode* newNode = BuyLTNode(val);
	prev->next = newNode;
	newNode->prev = prev;

	newNode->next = next;
	next->prev = newNode;
}

2 . 删除链表中的已有节点

1) 头删

// 链表头删
void LTPopFront(ListNode* phead)
{      // 还需要判断phead是否为空
	ListNode* cur = phead->next;
	if (cur == phead)
	{
		perror("链表中已无节点, 无法继续删除");
		exit(-1);
	}
	phead->next = cur->next;
	cur->next->prev = phead;
	delete cur;
	cur = NULL;
}

2) 尾删

// 链表尾删
void LTPopBack(ListNode* phead)
{       // 还需要判断 phead是否为空
	ListNode* tail = phead->prev;
	if (tail == phead) // 这里判断是否为空表
	{
		perror("链表中已无节点, 无法继续删除");
		exit(-1);
	}
	phead->prev = tail->prev;
	tail->prev->next = phead;
	delete tail;
	tail = NULL;
}

3) 删除指定位置的节点

//  链表 指定位置删除
void LTErase(ListNode* pos)
{   // 在这里可以增加一步实现, 判断pos是否等于phead , 这就需要传入第二个参数 phead
	if (!pos)
	{
		perror("pos is NULL");
		exit(-1);
	}
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;

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

3 . 链表的修改

1) 修改链表中指定节点的值

链表不同于顺序表 , 知道节点的位置, 修改其data值十分简单。

//  修改链表中指定节点的data值
void LTModify(ListNode* pos, LTDataType val)
{    // 在这里可以增加一步实现, 判断pos是否等于phead , 这就需要传入第二个参数 phead
	if (!pos)
	{
		perror("pos is NULL");
		exit(-1);
	}
	pos->data = val;
}

4 . 链表的查找

1) 查询链表中是否存在数据域为指定值的节点

// 链表 查找指定值
ListNode* LTFind(ListNode* phead, LTDataType val)
{            //  判断 phead是否为空  , 也就是是否存在哨兵头节点
	ListNode* cur = phead->next;
	while (cur != phead)    // 还需要判断链表是否为空
	{
		if (cur->data == val)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

2) 查询链表中节点的个数

这里所查询的节点的个数指除哨兵头节点之外的节点的个数。
即 数据域正常存储数据的节点的个数

// 链表 返回链表中有效节点(数据节点)的个数
int LTSize(ListNode* phead)
{
	// 判断 phead是否为空 ,    也就是是否存在哨兵节点(链表是否已初始化)
	int size = 0;
	ListNode* cur = phead->next;
	while (cur!= phead)
	{
		cur = cur->next;
		++size;
	}
	return size;
}

四 . 打印链表各个节点的数据域

// 链表打印
void LTPrint(ListNode* phead)
{
	if (!phead)
	{
		perror("phead is NULL");
		exit(-1);
	}

	ListNode* cur = phead->next;
	if (cur == phead)
	{
		cout << "空表" << endl;
		return;
	}
	cout << "phead <=> ";
	while (cur!=phead)
	{
		cout << cur->data << " <=> ";
		cur = cur->next;
	}
}

五 . 链表的销毁

// 链表销毁
ListNode* LTDestroy(ListNode* phead)
{    // 该函数可以定义为无返回值。 
	ListNode* cur = phead->next;
	ListNode* prev = NULL;
	while (cur != phead)
	{
		prev = cur;
		cur = cur->next;
		delete prev;
		prev = NULL;
	}
	delete phead;  // 哨兵头节点也需要释放其内存
	return NULL;
}

phead = LTDestroy(phead);

六 . 链表的优缺点

  • 优点 :
    • 任意位置元素的插入删除效率高
    • 按需申请或释放内存, 不存在空间的浪费
  • 缺点 :
    - 存储的数据不支持随机访问(不可使用下标进行访问)
    - 物理上存储空间是随机的,不连续。 CPU高速缓存命中率低
    -(也就是 存储不连续,导致缓存利用率低)

七 . 顺序表与链表的区别

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

相同点 :

  • 都是线性结构, 线性存储数据, 结构简单, 都可被称为线性表
  • 线性表的顺序存储 – 顺序表
  • 线性表的链式存储 – 链表
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值