文章目录
前言
使用单链表解决顺序表缺陷时,我们发现作为另一种形态出现的单链表似乎也有明显的缺陷。
- 在部分功能实现时因为头结点的改变需要引进二级指针(或者采用返回等更为复杂的方法)导致代码更加复杂。
- 尾部以及指定位置插入、删除数据的时间复杂度为O(N) ,效率低下;
1、带头双向循环链表介绍
无头单向非循环链表:结构简单,一般不会用来存储数据而是作为其他数据结构的子结构进行应用。
对比单链表来了解,带头双向循环链表:结构复杂,一般单独存储数据。凭着其复杂的结构我们可以做到快速管理数据,实现对数据的操作。
以下,带头双向循环链表简称双链表。
2、双链表的实现
2.1创建结构体
//创建结构体
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next; //后继指针
struct ListNode* prev; //前驱指针
LTDataType data; //数据域
}LTNode;
2.2双链表接口
//创建结点
LTNode* BuyLTNode(LTDataType x);
//初始化
LTNode* LTInit(); //使用返回值,避免使用二级指针
//打印
void LTPrint(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之前插入pos
void LTInsert(LTNode* pos, LTDataType x);
//指定位置删除
void LTErase(LTNode* pos);
//判空
bool LTEmpty(LTNode* phead);
//链表元素个数
size_t LTSize(LTNode* phead);
//销毁
void LTDestroy(LTNode* phead);
2.3创建结点
//创建结点并返回
LTNode* BuyLTNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
2.4初始化双链表
初始化链表时,需要注意由于带头,所以首先申请一个头结点(哨兵位),并且完成自我循环。
//初始化(由于哨兵位也需要创建,这里不可避免遇到指针级别问题,可以使用返回值带出去)
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
2.5增加节点
2.5.1头插
头插就是在哨兵位与第一个节点之间插入新节点,按照思路,将三方的相关指针改变指向即可。
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x); //申请结点
LTNode* first = phead->next; //保存哨兵位后第一个节点
//哨兵位指向新节点(由于哨兵位不存储有效数据,所以哨兵位不需要改变,变相达 到一种不对头结点操作的操作)
phead->next = newnode;
newnode->prev = phead; //新节点前驱指针指向哨兵位
newnode->next = first; //新节点后继指针指向保存的原第一节点
first->prev = newnode; //保存节点前驱结点指向新节点
}
2.5.2尾插
尾插就是将新节点插入头结点与头结点前一个结点之间的操作,而循环帮助我们不需要遍历链表找尾。
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x); //申请新节点
LTNode* tail = phead->prev; //保存尾结点
//newnode与原尾结点双向关系
tail->next = newnode;
newnode->prev = tail;
//newnode与哨兵位(头结点)双向关系
newnode->next = phead;
phead->prev = newnode;
}
2.5.3指定位置插入节点
指定位置插入可以理解为将新节点插入指定位置结点和插入位置结点之前一个节点之间。
//指定位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev; //保存指定位置节点前一个结点
LTNode* newnode = BuyLTNode(x); //申请新节点
//新节点与前一个结点双向关系
prev->next = newnode;
newnode->prev = prev;
//新节点与指定位置结点双向关系
newnode->next = pos;
pos->prev = newnode;
}
2.6删除结点
2.6.1头删
头删进行删除头结点与第二个有效节点之间的结点
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead); //判断是否只有头结点
LTNode* first = phead->next; //保存头结点之后第一个节点
LTNode* second = first->next; //保存first结点后的第一个节点
free(first); //释放删除位置节点
//头结点与第二个节点之间的双向关系
phead->next = second;
phead = second->prev;
}
2.6.2尾删
尾删进行释放最后一个节点并建立头结点与尾结点前一个结点之间的关系即可。
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
//保存需要操作的两个结点位置
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
//删除后尾结点与头结点的双向关系
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
}
2.6.3指定位置删除结点
指定位置删除结点就是释放指定位置结点并建立该节点前后两个节点的关系即可。
//指定位置删除
void LTErase(LTNode* pos)
{
assert(pos);
//保存位置
LTNode* prev = pos->prev;
LTNode* next = pos->next;
free(pos);
//建立双向关系
prev->next = next;
next->prev = prev;
//free(pos);也可以选择之后释放
}
指定位置的插入与删除可以直接复用到头部和尾部的操作中,注意参数即可。
2.7其余功能
2.7.1判空双链表
//判空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead; //检测头结点是否自我循环
}
2.7.2双链表元素个数
元素个数:统计除头结点之外的所有元素个数
//元素个数
size_t LTSize(LTNode* phead)
{
assert(phead);
size_t size = 0;
//先保存(位置)后遍历
LTNode* cur = phead->next;
while (cur != phead)
{
++size;
cur = cur->next;
}
return size;
}
2.7.3打印双链表
//打印
void LTPrint(LTNode* phead)
{
assert(phead);
//先保存后遍历(边遍历边打印)
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
2.7.4查找结点
//查找
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.7.5销毁双链表
//销毁
void LTDestroy(LTNode* phead)
{
assert(phead);
//遍历销毁
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur); //释放结点
cur=next;
}
free(phead);
}
3、双链表分析
优势:
- 按需申请空间;
- 任意位置插入删除效率很高;
劣势:
- 不支持随机访问 (下标访问);
- 相比于顺序表结构,CPU 高速缓存命中率低;
场景:
- 频繁的任意位置插入删除数据。