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);
到这里我们链表的相关知识就结束啦~
那么我们思考一个问题🤔🤔🤔
链表和顺序表(数组)有哪些区别和联系:
-
顺序表就是在数组的基础上实现增删查改,并且插入时可以动态增长
顺序表缺陷
:
a、可能存在一定空间浪费
b、增容有一些效率损失
c、中间或者头部插入删除,时间复杂度为O(N),因为要挪动数据 -
这些问题谁来解决了,链表。
链表的缺陷
:
不能随机访问
(二者是互补的数据结构)
🎉🎉🎉
结束啦~
友友们🤪🤪🤪
我们下期见~╰(°▽°)╯