*
博客主页:云曦
系列专栏:数据结构吾生也有涯,而知也无涯 感谢大家点赞 关注评论
在上期我们学了单链表,但单链表有个缺点就是尾插尾删不方便,所以这期我们要学习到一个非常完美的链表:带头双向循环链表。至于有多完美大家看完就知道了。
双链表含义其实就是每个节点可以找到前后的节点。而双链表也分为带头或不带头、循环或非循环,本章用的是带头且循环的双链表,称为:带头双向循环链表
结构:注意:
1. 链式结果只是在逻辑上是连续的,但在物理上就未必是连续的了。
2. 链表的节点是从堆上申请的空间。
3.从堆上申请的空间,可能是连续的也有可能不连续。
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;//上一个节点的地址
struct ListNode* next;//下一个节点的地址
LTDataType data;//存储数据
}LTNode;
此链表是带头+双向+循环的链表
想要使用链表要先创建获取节点的接口
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;
}
因为是带头的双链表,所以要初始化一下
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;
}
pritnf("\n");
}
注意:
因为之前实现单链表的时候,是无头的,但这里是带头的双向链表,所以遍历双链表的条件是判断是否等于头
节点的获取、初始化链表和打印的接口都有了,接下来就是实现尾插的接口了。
现在是双链表且带头,实现起来就简单多了,实现思路:
先让尾节点的next指向新节点,新节点的prev指向尾节点,最后让新节点的next指向头,让头的prev指向新节点。
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;
}
有了插入数据的接口,自然要有删除的接口。
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;
}
头插的实现思路也很简单:
将front->prev指向newnode,然后让newnode->next指向front,最后让phead->next指向newnode,让newnode->prev指向phead。
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;
}
头删也简单:定义front和frontNext两个指针,将phead->next指向frontNext,然后将frontNext->prev指向头,最后释放掉front。
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;
}
链表查找直接遍历一遍链表即可,找到了返回这个节点,找不到返回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;
}
实现思路:定义一个posprev的指针,将newnode->next指向pos,再让pos->prev指向newnode,然后将posprev->next指向newnode,最后让newnode->prev指向posprev。
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;
}
实现思路:定义在pos前后节点的两个指针,将posprev->next指向posnext,然后让posnext->prev指向posprev,最后释放掉pos。
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;
}
注意:这里的销毁指的是:把空间还给操作系统了。
思路:复用一下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);
}
复用LTInsert接口,将头的下一个节点和x传入即可。
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);//断言
//复用在pos位置前面插入节点的接口
LTInsert(phead->next, x);
}
复用一下LTInsert接口即可,参数传phead->prev和x,逻辑上是在头前面插入,但在物理上是在尾部插入,因为头上一个的节点就是尾节点。
void LTpushBack(LTNode* phead, LTDataType x)
{
assert(phead);//断言
//复用在pos位置前面插入节点的接口
LTInsert(phead->prev, x);
}
复用LTErase接口,把头的下一个节点传入即可
void LTPopFront(LTNode* phead)
{
assert(phead);//断言
//检查是否只有哨兵卫了
assert(phead != phead->next);
//复用删除pos位置节点的接口
LTErase(phead->next);
}
复用LTErase接口,把头上一个节点传入即可
void LTPopBack(LTNode* phead)
{
assert(phead);//断言
//检查是否只有哨兵卫了
assert(phead != phead->next);
//复用删除pos位置节点的接口
LTErase(phead->prev);
}
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理一定连续 | 逻辑上连续,物理上不一定连续 |
随机访问 | 支持:时间复杂度为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);
}