双向循环带头链表
一、什么是双向循环带头链表?
1. 概念
双向循环带头链表(Doubly Circular Linked List with Head Node) 是一种结合了双向指针、循环结构和**头节点(哨兵节点)**的复杂链表形式,兼具灵活性与操作便捷性。
2. 结构
链表整体结构:
链表节点结构:
双向链表相对于单链表来说,除了数据域和指向下一个节点的后继指针,还多了一个指向前一个节点的前驱指针。
typedef int DLTDataType;
typedef struct DListNode
{
DLTDataType val; // 数据域
struct DListNode* prev; // 前驱指针
struct DListNode* next; // 后继指针
}DLTNode;
3. 与单链表的比较
3.1 结构与内存占用对比
特性 | 双向循环链表 | 单链表 |
---|---|---|
节点结构 | 包含 prev 和 next 两个指针 | 仅包含 next 指针 |
循环性 | 尾节点 next 指向头节点,头节点 prev 指向尾节点 | 尾节点 next 指向 NULL |
内存占用 | 每个节点多一个指针(空间复杂度 O(n) 更高) | 空间占用较低 |
3.2 操作复杂度与效率对比
操作 | 双向循环链表 | 单链表 |
---|---|---|
插入/删除 | 任意位置 O(1)(直接操作前驱和后继指针) | 已知前驱节点时 O(1),否则需遍历 O(n) |
遍历方向 | 支持双向遍历(正向/反向) | 仅支持单向遍历(从头到尾) |
头尾操作 | 头插/头删、尾插/尾删均 O(1) | 头插/头删、尾插/尾删均 O(1) |
查找特定节点 | 双向遍历可能减少平均查找时间(灵活方向) | 必须从头开始遍历 O(n) |
二、双向循环带头链表的实现
1. 初始化一个双线循环带头链表
申请一个节点
DLTNode* DLTBuyNode(DLTDataType x)
{
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
node->val = x;
node->prev = node; // 前驱和后继都指向自己
node->next = node;
}
初始化链表
DLTNode* InitLink()
{
//return DLTBuyNode(0); // 因为后续操作和申请节点的操作完全一样,所以其实可以直接调用申请节点
DLTNode* phead = (DLTNode*)malloc(sizeof(DLTNode));
phead->val = 0;
// 初始化时,虚拟头节点的前驱和后继指针均指向自己
phead->prev = phead;
phead->next = phead;
return phead;
}
为什么虚拟头节点的前驱和后继指针都指向自己呢?
假设存在多个节点,那么虚拟头节点的前驱指向的是链表尾节点,此时只有它自己,所以它自己就是自己的尾
同理,尾节点的后继指向的是链表的头节点,它既是头也是尾,所以均指向自己
2. 尾插
因为是双向循环带头链表,所以虚拟头节点的前驱即为尾节点。
那么要修改几个指针呢?
四个!新节点的前驱和后继指针、虚拟头节点的前驱指针、尾节点的前驱指针。
我们又该以什么样的顺序去修改这四个指针呢?
个人建议先修改newnode的两个指针,再去修改头尾节点,对于不熟的同学来说,这样能在一定程度上避免链表断连。
void DLTPushBack(DLTNode* phead, DLTDataType x)
{
DLTNode* node = DLTBuyNode(x);
// 我建议先修改新节点的指针
// 总共需要修改四个指针
// 新节点的后继为头节点
node->next = phead;
// 新节点的前驱为原来的尾节点
node->prev = phead->prev;
// 原来尾节点的后继为新节点
phead->prev->next = node;
// 头节点的前驱为新节点
phead->prev = node;
}
3. 头插
双向循环带头链表相比于单链表来说,虽然看着多了个指针,貌似结构会更复杂,但是链表的插入和删除极其简单,所以后续便不做过多赘述,都参考尾插和尾删即可。实在是不理解的话,那还是通过画图去理解吧。
void DLTPushFront(DLTNode* phead, DLTDataType x)
{
DLTNode* node = DLTBuyNode(x);
// 新节点的后继为原来的头节点
node->next = phead->next;
// 新节点的前驱为虚拟头节点
node->prev = phead;
// 原来头节点的前驱为新节点
phead->next->prev = node;
// 虚拟头节点的后继为新节点
phead->next = node;
}
4. 尾删
先用cur记录尾节点,再将虚拟头节点的前驱置为尾节点的前驱,尾节点的前驱的后继(新尾节点的后继)置为虚拟头节点,最后释放cur。
void DLTPopBack(DLTNode* phead)
{
if (phead->prev == phead)
{
perror("The linked list is empty\n");
exit(1);
}
// 记录尾节点
DLTNode* cur = phead->prev;
// 新尾节点为原尾节点的前驱
phead->prev = cur->prev;
// 新尾节点的前驱为虚拟头节点
cur->prev->next = phead;
free(cur);
}
5. 头删
void DLTPopFront(DLTNode* phead)
{
if (phead->next == phead)
{
perror("The linked list is empty\n");
exit(1);
}
// 记录头节点
DLTNode* cur = phead->next;
// 虚拟头节点的后继为头节点的后继
phead->next = phead->next->next;
// 新头节点的前驱为虚拟头节点
cur->next->prev = phead;
free(cur);
}
6. 查找
DLTNode* DLTFind(DLTNode* phead, DLTDataType x)
{
DLTNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
7. 在指定位置之前插入数据
void DLTInsert(DLTNode* phead, DLTNode* pos, DLTDataType x)
{
DLTNode* node = DLTBuyNode(x);
// 新节点的后继为pos
node->next = pos;
// 新节点的前驱为pos的前驱
node->prev = pos->prev;
// pos的前驱的后继为新节点
pos->prev->next = node;
// pos的前驱为新节点
pos->prev = node;
}
8. 删除pos节点
void DLTErase(DLTNode* pos)
{
// pos的前驱的后继为pos的后继
pos->prev->next = pos->next;
// pos的后继的前驱为pos的前驱
pos->next->prev = pos->prev;
free(pos);
}
9. 销毁链表
// 上层调用时记得手动置空虚拟头节点
void DLTDesTroy(DLTNode* phead)
{
DLTNode* cur = phead->next;
while (cur != phead)
{
DLTNode* prev = cur;
cur = cur->next;
free(prev);
}
free(phead);
}
三、源码
还是老三样:头文件、源文件、测试文件
DList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int DLTDataType;
typedef struct DListNode
{
DLTDataType val;
struct DListNode* prev;
struct DListNode* next;
}DLTNode;
// 打印
void DLTPrint(DLTNode* phead);
// 初始化一个双向带头循环链表
DLTNode* InitLink();
// 尾插
void DLTPushBack(DLTNode* phead, DLTDataType x);
// 头插
void DLTPushFront(DLTNode* phead, DLTDataType x);
// 尾删
void DLTPopBack(DLTNode* phead);
// 头删
void DLTPopFront(DLTNode* phead);
// 查找
DLTNode* DLTFind(DLTNode* phead, DLTDataType x);
//在指定位置之前插入数据
void DLTInsert(DLTNode* phead, DLTNode* pos, DLTDataType x);
// 删除pos节点
void DLTErase(DLTNode* pos);
//销毁链表
void DLTDesTroy(DLTNode* phead);
DLst.c
#include "DList.h"
DLTNode* DLTBuyNode(DLTDataType x)
{
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
node->val = x;
node->prev = node;
node->next = node;
}
void DLTPrint(DLTNode* phead)
{
DLTNode* cur = phead->next;
while (cur != phead)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
DLTNode* InitLink()
{
//return DLTBuyNode(0);
DLTNode* phead = (DLTNode*)malloc(sizeof(DLTNode));
phead->val = 0;
// 初始化时,虚拟头节点的前驱和后继指针均指向自己
phead->prev = phead;
phead->next = phead;
return phead;
}
void DLTPushBack(DLTNode* phead, DLTDataType x)
{
DLTNode* node = DLTBuyNode(x);
// 我建议先修改新节点的指针
// 总共需要修改四个指针
// 新节点的后继为头节点
node->next = phead;
// 新节点的前驱为原来的尾节点
node->prev = phead->prev;
// 原来尾节点的后继为新节点
phead->prev->next = node;
// 头节点的前驱为新节点
phead->prev = node;
}
void DLTPushFront(DLTNode* phead, DLTDataType x)
{
DLTNode* node = DLTBuyNode(x);
// 新节点的后继为原来的头节点
node->next = phead->next;
// 新节点的前驱为虚拟头节点
node->prev = phead;
// 原来头节点的前驱为新节点
phead->next->prev = node;
// 虚拟头节点的后继为新节点
phead->next = node;
}
void DLTPopBack(DLTNode* phead)
{
if (phead->prev == phead)
{
perror("The linked list is empty\n");
exit(1);
}
// 记录尾节点
DLTNode* cur = phead->prev;
// 新尾节点为原尾节点的前驱
phead->prev = cur->prev;
// 新尾节点的前驱为虚拟头节点
cur->prev->next = phead;
free(cur);
}
void DLTPopFront(DLTNode* phead)
{
if (phead->next == phead)
{
perror("The linked list is empty\n");
exit(1);
}
// 记录头节点
DLTNode* cur = phead->next;
// 虚拟头节点的后继为头节点的后继
phead->next = phead->next->next;
// 新头节点的前驱为虚拟头节点
cur->next->prev = phead;
free(cur);
}
DLTNode* DLTFind(DLTNode* phead, DLTDataType x)
{
DLTNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void DLTInsert(DLTNode* phead, DLTNode* pos, DLTDataType x)
{
DLTNode* node = DLTBuyNode(x);
// 新节点的后继为pos
node->next = pos;
// 新节点的前驱为pos的前驱
node->prev = pos->prev;
// pos的前驱的后继为新节点
pos->prev->next = node;
// pos的前驱为新节点
pos->prev = node;
}
void DLTErase(DLTNode* pos)
{
// pos的前驱的后继为pos的后继
pos->prev->next = pos->next;
// pos的后继的前驱为pos的前驱
pos->next->prev = pos->prev;
free(pos);
}
// 上层调用时记得手动置空虚拟头节点
void DLTDesTroy(DLTNode* phead)
{
DLTNode* cur = phead->next;
while (cur != phead)
{
DLTNode* prev = cur;
cur = cur->next;
free(prev);
}
free(phead);
}
test.c
#include "DList.h"
void test_PushBack()
{
DLTNode* phead = InitLink();
DLTPushBack(phead, 1);
DLTPushBack(phead, 2);
DLTPushBack(phead, 3);
DLTPushBack(phead, 4);
DLTPushBack(phead, 5);
DLTPrint(phead);
}
void test_PushFront()
{
DLTNode* phead = InitLink();
DLTPushFront(phead, 1);
DLTPushFront(phead, 2);
DLTPushFront(phead, 3);
DLTPushFront(phead, 4);
DLTPushFront(phead, 5);
DLTPrint(phead);
}
void test_PopBack()
{
DLTNode* phead = InitLink();
DLTPushBack(phead, 1);
DLTPushBack(phead, 2);
DLTPushBack(phead, 3);
DLTPushBack(phead, 4);
DLTPushBack(phead, 5);
DLTPrint(phead);
DLTPopBack(phead);
DLTPopBack(phead);
DLTPrint(phead);
}
void test_DLTInsert()
{
DLTNode* phead = InitLink();
DLTPushBack(phead, 1);
DLTPushBack(phead, 2);
DLTPushBack(phead, 3);
DLTPushBack(phead, 4);
DLTPushBack(phead, 5);
DLTPrint(phead);
DLTNode* cur = DLTFind(phead, 3);
DLTInsert(phead, cur, 6);
DLTPrint(phead);
}
void test_DLTErase()
{
DLTNode* phead = InitLink();
DLTPushBack(phead, 1);
DLTPushBack(phead, 2);
DLTPushBack(phead, 3);
DLTPushBack(phead, 4);
DLTPushBack(phead, 5);
DLTPrint(phead);
DLTNode* cur = DLTFind(phead, 3);
DLTErase(cur);
DLTPrint(phead);
}
void test_DLTDesTroy()
{
DLTNode* phead = InitLink();
DLTPushBack(phead, 1);
DLTPushBack(phead, 2);
DLTPushBack(phead, 3);
DLTPushBack(phead, 4);
DLTPushBack(phead, 5);
DLTPrint(phead);
DLTDesTroy(phead);
phead = NULL;
}
int main()
{
//test_PushBack();
//test_PushFront();
//test_PopBack();
//test_DLTInsert();
//test_DLTErase();
test_DLTDesTroy();
return 0;
}