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

在这里插入图片描述

hello 友友们~
今天我们来学习数据结构里面的带头双向循环链表叭~🤪🤪🤪

1.带头双向循环链表的定义

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

在这里插入图片描述

2.创建结点

//创建结点
ListNode* BuyListNode(LTDataType* x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	//初始化
	node->next = NULL;
	node->prev = NULL;
	node->data = x;

	return node;
}

3.打印

void ListPrint(ListNode* phead)
{
	assert(phead);
	ListNode* cur = phead->next;  // 从第一个有效节点开始
	while (cur != phead)// 当回到头节点时停止
	{
		printf("%d ", cur->data);
		cur = cur->next; // 移动到下一个节点
	}
}

画图分析:
在这里插入图片描述

4.结点初始化

//初始化
ListNode* ListInit()
{
	ListNode* phead = BuyListNode(0);//创建头节点
	//这里传入参数0可能只是一个占位值,因为头节点通常不存储有效数据。phead 是指向新创建的头节点的指针。

    //形成一个只有一个节点的循环链表
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

//调用
int main()
{
	ListNode* phead = ListInit();
	return 0;
}

ListInit 函数的主要目的是创建一个双向循环链表的头节点,并对其进行初始化,最后返回指向该头节点的指针。函数的返回值设计使得它可以直接将新创建的头节点指针传递给调用者,调用者通过接收返回值就能获取到指向链表头节点的指针,从而可以对链表进行后续操作。

注意: ❗❗❗

在上述代码中,ListInit 函数返回了指向创建头节点的指针main 函数通过接收这个返回值,将其赋值给phead变量,这样 phead 就指向了初始化好的链表头节点,后续可以基于这个指针来操作链表。

5.尾插

void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);

	ListNode* tail = phead->prev;//找到链表的尾节点
	ListNode* newnode = BuyListNode(x);//创建新节点
    
    //插入新节点到链表尾部
	tail->next = newnode;//将原尾节点的 next 指针指向新节点
	newnode->prev = tail;// 将新节点的 prev 指针指向原尾节点

	newnode->next = phead;//将新节点的 next 指针指向头节点
	phead->prev = newnode;//将头节点的 prev 指针指向新节点

}

画图分析
在这里插入图片描述

6.尾删

//尾删
void ListPopBack(ListNode* phead)
{
	assert(phead);
    assert(phead->next != phead);//断言一下表示不删除头结点,否则会报错

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

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

画图分析:
在这里插入图片描述

7.头插

注意:这里的头插指的是在哨兵位后面那个位置进行头插,因为哨兵位是用来展位不计的

//头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);

	ListNode* first = phead->next;
	ListNode* newnode = BuyListNode(x);

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

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

画图分析:
在这里插入图片描述

8.头删

❗❗❗注意:头删并不是删除头结点,因为在这个双向链表中无论怎么操作都不能删除头点!!

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

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

	//phead  first  second
	phead->next = second;
	second->prev = phead;
	free(first);
}

画图分析:
在这里插入图片描述

9.查找

//双向链表查找(查找x)
ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

10.在pos的前面进行插入

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* posPrev = pos->prev;
	ListNode* newnode = BuyListNode(x);

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

画图分析:
在这里插入图片描述

11.删除pos位置的节点

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* posPrev = pos->prev;
	ListNode* posNext = pos->next;

	free(pos);
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

这里就不画图啦~和上面同理!

12.pos位置增删查改代码的实现

在这里插入图片描述

13.内存释放

这里有两种情况:
1.是完全消除整个链表
2.是清空所有数据

//清理所有插入数据
void ListClear(ListNode* phead)
{
	assert(phead);
	//清理所有数据节点,保留头结点,可以继续使用
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	phead->next = phead;
	phead->prev = phead;
}


//释放
//这里不用二级指针保持了代码的一致性
void ListDestory(ListNode* phead)
{
	assert(phead);
	ListClear(phead);
	free(phead);
}

14.学了pos位置后的替换

上面我们学了pos位置处的插入删除,那么我们就可以调用insert和Erase函数来简化上面尾插、尾删、头插、头删处的代码了!💡💡💡

//头插
ListInsert(phead->next, x);//也就是在第一个结点前面插,就是头插
//尾插
ListInsert(phead, x);//在phead的前面插就是尾
//尾删
ListErase(phead->prev);
//头删
ListErase(phead->next);

15.接口实现

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

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

//void ListInit(ListNode** pphead);
ListNode* ListInit(); 
//清理所有插入数据
void ListClear(ListNode* phead);

//释放销毁整个程序
void ListDestory(ListNode** pphead);

//创建结点
ListNode* BuyListNode(LTDataType* x);
//打印
void ListPrint(ListNode* phead);
//尾插
void ListPushBack(ListNode* phead, LTDataType x);
//尾删
void ListPopBack(ListNode* phead);
//头插
void ListPushFront(ListNode* phead, LTDataType x);
//头删
void ListPopFront(ListNode* phead);
//双向链表查找(查找x)
ListNode* ListFind(ListNode* phead, LTDataType x);//链表可以返回结点的指针
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

到这里我们链表的相关知识就结束啦~

那么我们思考一个问题🤔🤔🤔
链表和顺序表(数组)有哪些区别和联系:

  1. 顺序表就是在数组的基础上实现增删查改,并且插入时可以动态增长
    顺序表缺陷
    a、可能存在一定空间浪费
    b、增容有一些效率损失
    c、中间或者头部插入删除,时间复杂度为O(N),因为要挪动数据

  2. 这些问题谁来解决了,链表
    链表的缺陷
    不能随机访问

(二者是互补的数据结构)

🎉🎉🎉

结束啦~
友友们🤪🤪🤪
我们下期见~╰(°▽°)╯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值