循环链表与双向链表
一、循环链表(Circular Linked List)
1. 基本概念
循环链表是一种特殊的链表,它将单链表最后一个节点的next指针指向头节点 或第一个元素,使链表形成一个闭环。
2. 结构特点
typedef struct Node {
ElemType data; // 数据域
struct Node *next; // 指针域
} Node, *LinkList;
3. 关键特性对比
| 特性 | 单链表 | 循环链表 |
|---|---|---|
| 尾节点指针 | 指向NULL | 指向头节点/首节点 |
| 遍历终止条件 | p != NULL 或 p->next != NULL |
p->next != Head |
| 空表判断 | head == NULL 或 head->next == NULL |
head->next == head(带头节点) |
| 结构形态 | 线性结构 | 环形结构 |
4. 常见形式
-
带头节点的循环链表(更常用):
-
空表:
head->next == head -
非空表:尾节点指向头节点
-
-
不带头节点的循环链表:
-
空表:
head == NULL -
非空表:尾节点指向首节点
-
5. 循环链表操作示例
// 初始化循环链表(带头节点)
Status InitList(LinkList *L) {
*L = (LinkList)malloc(sizeof(Node));
if (*L == NULL) return ERROR;
(*L)->next = *L; // 指向自身,形成环
return OK;
}
// 判断链表是否为空(带头节点)
int ListEmpty(LinkList L) {
return (L->next == L);
}
// 遍历循环链表
void TraverseList(LinkList L) {
if (L->next == L) {
printf("链表为空\n");
return;
}
LinkList p = L->next; // 从第一个节点开始
while (p != L) { // 回到头节点时结束
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 在循环链表中查找元素
Node* LocateElem(LinkList L, ElemType e) {
if (L->next == L) return NULL;
LinkList p = L->next;
while (p != L) {
if (p->data == e) return p;
p = p->next;
}
return NULL;
}
6. 约瑟夫环问题(循环链表典型应用)
// 约瑟夫环问题求解
void Josephus(int n, int m) {
// 创建循环链表
Node *head = (Node*)malloc(sizeof(Node));
Node *prev = head;
// 构建循环链表
for (int i = 1; i <= n; i++) {
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->data = i;
prev->next = newNode;
prev = newNode;
}
prev->next = head->next; // 形成环
// 开始约瑟夫环计数
Node *current = head->next;
Node *pre = prev; // current的前驱
printf("出列顺序:");
while (current->next != current) {
// 报数m-1次
for (int count = 1; count < m; count++) {
pre = current;
current = current->next;
}
// 删除当前节点
printf("%d ", current->data);
pre->next = current->next;
free(current);
current = pre->next;
}
printf("%d\n", current->data);
free(current);
free(head);
}
二、双向链表(Double Linked List)
1. 基本结构
typedef struct DulNode {
ElemType data; // 数据域
struct DulNode *prior; // 前驱指针
struct DulNode *next; // 后继指针
} DulNode, *DuLinkList;
2. 结构示意图
prior data next
NULL ← [Node] → [Node] → [Node] → NULL
↑ ↑ ↑
next prior prior
3. 双向链表操作
初始化
// 初始化双向链表(带头节点)
Status InitDuList(DuLinkList *L) {
*L = (DuLinkList)malloc(sizeof(DulNode));
if (*L == NULL) return ERROR;
(*L)->prior = NULL;
(*L)->next = NULL;
return OK;
}
插入操作
// 在节点p之后插入新节点s
Status InsertAfterNode(DulNode *p, DulNode *s) {
if (p == NULL || s == NULL) return ERROR;
s->prior = p;
s->next = p->next;
if (p->next != NULL) {
p->next->prior = s;
}
p->next = s;
return OK;
}
// 在节点p之前插入新节点s
Status InsertBeforeNode(DulNode *p, DulNode *s) {
if (p == NULL || s == NULL) return ERROR;
s->next = p;
s->prior = p->prior;
if (p->prior != NULL) {
p->prior->next = s;
}
p->prior = s;
return OK;
}
删除操作
// 删除节点p
Status DeleteNode(DulNode *p) {
if (p == NULL) return ERROR;
if (p->prior != NULL) {
p->prior->next = p->next;
}
if (p->next != NULL) {
p->next->prior = p->prior;
}
free(p);
return OK;
}
遍历操作
// 前向遍历(从尾到头)
void TraverseBackward(DuLinkList L) {
if (L == NULL || L->next == NULL) {
printf("链表为空\n");
return;
}
// 找到尾节点
DulNode *p = L->next;
while (p->next != NULL) {
p = p->next;
}
// 从后向前遍历
while (p != L) {
printf("%d ", p->data);
p = p->prior;
}
printf("\n");
}
// 后向遍历(从头到尾)
void TraverseForward(DuLinkList L) {
if (L == NULL || L->next == NULL) {
printf("链表为空\n");
return;
}
DulNode *p = L->next;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
4. 双向循环链表
双向链表也可以形成循环结构:
// 初始化双向循环链表
Status InitDuCircularList(DuLinkList *L) {
*L = (DuLinkList)malloc(sizeof(DulNode));
if (*L == NULL) return ERROR;
(*L)->prior = *L; // 前驱指向自身
(*L)->next = *L; // 后继指向自身
return OK;
}
// 判断双向循环链表是否为空
int DuCircularListEmpty(DuLinkList L) {
return (L->next == L && L->prior == L);
}
三、对比总结
| 特性 | 单链表 | 循环链表 | 双向链表 | 双向循环链表 |
|---|---|---|---|---|
| 指针数量 | 1个(next) | 1个(next) | 2个(prior, next) | 2个(prior, next) |
| 空间复杂度 | O(n) | O(n) | O(2n) | O(2n) |
| 遍历方向 | 单向 | 单向循环 | 双向 | 双向循环 |
| 查找效率 | O(n) | O(n) | O(n)但可双向查找 | O(n)但可双向查找 |
| 插入/删除 | 需知道前驱 | 需知道前驱 | 可直接操作,无需前驱 | 可直接操作,无需前驱 |
| 典型应用 | 一般线性存储 | 约瑟夫环、轮询调度 | 浏览器历史记录、文本编辑器 | 循环缓冲区、高级调度 |
四、实际应用场景
循环链表的应用:
-
操作系统:时间片轮转调度算法
-
游戏开发:玩家轮流回合
-
多媒体:循环播放列表
-
计算机网络:令牌环网络
双向链表的应用:
-
文本编辑器:撤销/重做功能
-
浏览器:前进/后退历史记录
-
LRU缓存:最近最少使用算法
-
音乐播放器:上一曲/下一曲功能
五、编程注意事项
-
循环链表的终止条件:避免无限循环,确保有明确的结束条件
-
双向链表的指针更新:插入和删除时要同时更新prior和next指针
-
内存管理:及时释放删除的节点,防止内存泄漏
-
边界处理:特别注意头节点和尾节点的特殊情况
-
空表判断:不同实现方式(带头节点/不带头节点)的判断条件不同