循环单链表的实现——2027考研408数据结构全攻略
适用范围:2027年全国硕士研究生招生考试·计算机学科专业基础(408)
考纲章节:第二章 线性表 → 2.3 线性表的链式表示 → 循环链表
难度等级:⭐⭐⭐⭐(进阶重点,理解记忆型考点多)
目录
一、知识点概念
1.1 循环链表定义
循环链表(Circular Linked List) 是链表的另一种形式,其特点是将链表尾结点的指针域由 NULL 改为指向链表的头结点(或第一个结点),使链表形成一个环。
循环链表分为两类:
- 循环单链表:由单链表首尾相连构成
- 循环双链表:由双链表首尾相连构成
本章重点讲解循环单链表。
1.2 循环单链表定义
循环单链表(Circular Singly Linked List) 是在单链表的基础上,将最后一个结点的 next 指针指向头结点(或第一个结点),形成一个环形结构。
普通单链表:
[头] → [a1] → [a2] → [a3] → NULL
循环单链表:
[头] ────────────────────┐
↓ │
[a1] → [a2] → [a3] ──────┘
1.3 核心特征
- 无 NULL 指针:遍历循环链表时,没有 NULL 作为终止条件,必须以某个结点的
next再次指向头结点作为循环终止条件。 - 从任意结点出发可遍历全表:不像普通单链表只能从头结点出发。
- 首尾相连:尾结点的
next指向头结点(带哨兵)或首元素(不带哨兵)。
1.4 带头结点的循环单链表
考研中通常使用带头结点的循环单链表,其特点:
- 头结点的
next指向第一个数据结点(若非空) - 当链表为空时,头结点的
next指向自己 - 尾结点的
next指向头结点
空表: [头] ──→ (指向自己)
非空表: [头] → [a1] → [a2] → [a3] → [头]
↑___________________↓ (尾结点next指向头结点)
1.5 不带头结点的循环单链表
- 不使用哨兵结点
- 头指针
L直接指向第一个数据结点 - 空表时
L = NULL - 尾结点的
next指向头指针
空表: L = NULL
非空: L → [a1] → [a2] → [a3] → [a1]
↑______↓ (尾结点next指向首结点)
📌 考研重点:考试中默认使用带头结点的循环单链表,需要熟练掌握空表与非空表的判断。
二、循环单链表的结构定义
2.1 C语言结构体定义
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType;
/* 循环单链表结点定义(与普通单链表完全相同) */
typedef struct LNode {
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *CircularLinkList;
⚠️ 注意:循环单链表的结点结构与普通单链表完全相同,区别仅在于遍历终止条件和尾结点指针的指向。
2.2 空表的两种表示
| 类型 | 空表判断条件 |
|---|---|
| 带头结点 | L->next == L(头结点指向自己) |
| 不带头结点 | L == NULL |
| 普通单链表(对比) | L->next == NULL |
三、循环单链表的实现(基本操作)
3.1 初始化(带头结点)
/* 初始化循环单链表(带头结点) */
bool InitList(CircularLinkList *L) {
*L = (CircularLinkList)malloc(sizeof(LNode));
if (*L == NULL) return false;
(*L)->next = *L; // 关键:头结点的next指向自己(空表)
return true;
}
/* 判空 */
bool ListEmpty(CircularLinkList L) {
return L->next == L; // 头结点指向自己即为空表
}
3.2 按位查找
/* 按位查找:返回第i个结点的指针(1-based,首元素为第1个)
* 关键:循环结束条件是 p != L(回到头结点)
*/
LNode* GetElem(CircularLinkList L, int i) {
if (i < 1) return NULL;
LNode *p = L->next; // p指向第一个数据结点(不是头结点!)
int j = 1;
// 关键:p != L 表示还没回到头结点
while (p != L && j < i) {
p = p->next;
j++;
}
// 退出循环的两种情况:
// 1. j == i:找到第i个结点,p即为结果
// 2. p == L:已遍历一圈未找到(i过大)
return (j == i) ? p : NULL;
}
遍历终止条件对比:
| 链表类型 | 遍历条件 | 说明 |
|---|---|---|
| 普通单链表(带头) | p != NULL | 以NULL为终止标志 |
| 普通单链表(不带头) | p != NULL | 以NULL为终止标志 |
| 循环单链表(带头) | p != L | 以头结点为终止标志 |
| 循环单链表(不带头) | p != NULL 且计数 | 需额外计数控制 |
3.3 按值查找
/* 按值查找:返回第一个值为e的结点的位序,不存在返回0 */
int LocateElem(CircularLinkList L, ElemType e) {
LNode *p = L->next; // 从第一个数据结点开始
int i = 1;
while (p != L) { // 未回到头结点则继续
if (p->data == e)
return i;
p = p->next;
i++;
}
return 0;
}
3.4 插入操作
3.4.1 按位序插入
/* 在第i个位置插入元素e
* 循环单链表的插入与普通单链表类似,但需注意循环终止条件
*/
bool ListInsert(CircularLinkList *L, int i, ElemType e) {
LNode *p = GetElem(*L, i - 1); // 找到第i-1个结点
if (p == NULL) return false; // i不合法
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s == NULL) return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
插入图解(在表尾插入):
插入前: [头] → [a1] → [a2] → [a3] → [头]
↑
p
插入e=99到末尾(i=4):
1. s->data = 99, s->next = p->next (即[头])
2. p->next = s
插入后: [头] → [a1] → [a2] → [a3] → [99] → [头]
3.4.2 尾插法(经典实现)
循环单链表的尾插法优势明显:不需要遍历到表尾,因为尾结点的 next 本身指向头结点,可以直接定位。
/* 尾插法创建循环单链表(高效实现) */
CircularLinkList List_TailInsert(CircularLinkList *L) {
InitList(L);
LNode *r = *L; // r指向头结点(也是尾结点)
ElemType x;
LNode *s;
printf("请输入元素(输入9999结束): ");
scanf("%d", &x);
while (x != 9999) {
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = r->next; // 新结点指向头结点
r->next = s; // 原尾结点指向新结点
r = s; // r移动到新结点(现为尾结点)
scanf("%d", &x);
}
return *L;
}
为什么循环单链表尾插效率高?
| 链表类型 | 尾插法时间复杂度 | 原因 |
|---|---|---|
| 普通单链表 | O(n) | 每次尾插需遍历到表尾 |
| 循环单链表 | O(1) | 尾结点 next 指向头结点,可直接定位 |
📌 考研重点:这是循环单链表的核心优势之一,尾插法时间复杂度为 O(1)!
3.5 删除操作
3.5.1 按位序删除
/* 删除第i个位置的元素,并用e返回其值 */
bool ListDelete(CircularLinkList *L, int i, ElemType *e) {
if (i < 1) return false;
LNode *p = GetElem(*L, i - 1); // 找到第i-1个结点
if (p == NULL || p->next == L) return false; // p->next是头结点则i超出范围
LNode *q = p->next; // q指向待删除结点
*e = q->data;
p->next = q->next; // 绕过q
free(q);
return true;
}
删除图解:
删除前: [头] → [a1] → [a2] → [a3] → [头]
↑
p,q(待删除)
删除a2:
1. *e = q->data = a2
2. p->next = q->next (指向a3)
3. free(q)
删除后: [头] → [a1] → [a3] → [头]
3.5.2 删除尾结点
/* 删除循环单链表的尾结点(需要找到尾结点的前驱) */
bool DeleteTail(CircularLinkList *L, ElemType *e) {
if (L->next == L) return false; // 空表
LNode *p = *L; // p指向头结点
// 找到尾结点的前驱(头结点)
while (p->next != *L) { // p的下一个是头结点时,p是尾结点
p = p->next;
}
// 此时p是尾结点(p->next == L)
if (p == *L) return false; // 只有一个头结点(空表)
LNode *q = p->next; // q指向第一个数据结点(若只有一个数据结点)
if (q == *L) { // 只有头结点,无数据结点
return false;
}
// 找到最后一个数据结点
while (q->next != *L) {
p = q;
q = q->next;
}
// 此时q是尾结点,p是q的前驱
*e = q->data;
p->next = q->next; // 前驱指向头结点
free(q);
return true;
}
3.6 求表长
/* 求循环单链表的表长 */
int ListLength(CircularLinkList L) {
int len = 0;
LNode *p = L->next; // p指向第一个数据结点
while (p != L) { // 未回到头结点则计数
len++;
p = p->next;
}
return len;
}
3.7 完整示例代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType;
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *CircularLinkList;
/* 初始化 */
bool InitList(CircularLinkList *L) {
*L = (CircularLinkList)malloc(sizeof(LNode));
if (*L == NULL) return false;
(*L)->next = *L; // 关键:指向自己
return true;
}
/* 判空 */
bool ListEmpty(CircularLinkList L) {
return L->next == L;
}
/* 按位查找 */
LNode* GetElem(CircularLinkList L, int i) {
if (i < 1) return NULL;
LNode *p = L->next;
int j = 1;
while (p != L && j < i) {
p = p->next;
j++;
}
return (j == i) ? p : NULL;
}
/* 按值查找 */
int LocateElem(CircularLinkList L, ElemType e) {
LNode *p = L->next;
int i = 1;
while (p != L) {
if (p->data == e) return i;
p = p->next;
i++;
}
return 0;
}
/* 插入 */
bool ListInsert(CircularLinkList *L, int i, ElemType e) {
LNode *p = GetElem(*L, i - 1);
if (p == NULL) return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s == NULL) return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
/* 删除 */
bool ListDelete(CircularLinkList *L, int i, ElemType *e) {
if (i < 1) return false;
LNode *p = GetElem(*L, i - 1);
if (p == NULL || p->next == L) return false;
LNode *q = p->next;
*e = q->data;
p->next = q->next;
free(q);
return true;
}
/* 表长 */
int ListLength(CircularLinkList L) {
int len = 0;
LNode *p = L->next;
while (p != L) {
len++;
p = p->next;
}
return len;
}
/* 尾插法创建 */
CircularLinkList List_TailInsert(CircularLinkList *L) {
InitList(L);
LNode *r = *L;
ElemType x;
printf("输入元素(9999结束): ");
scanf("%d", &x);
while (x != 9999) {
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = r->next;
r->next = s;
r = s;
scanf("%d", &x);
}
return *L;
}
/* 打印循环单链表 */
void PrintList(CircularLinkList L) {
printf("循环单链表: ");
if (L->next == L) {
printf("(空表)\n");
return;
}
LNode *p = L->next;
while (p != L) {
printf("%d ", p->data);
p = p->next;
}
printf("(长度=%d)\n", ListLength(L));
}
/* 主函数测试 */
int main() {
CircularLinkList L;
ElemType e;
// 测试初始化和尾插法创建
List_TailInsert(&L); // 输入: 10 20 30 40 50 9999
PrintList(L); // 输出: 10 20 30 40 50
// 插入
ListInsert(&L, 3, 25); // 在第3位插入25
PrintList(L); // 输出: 10 20 25 30 40 50
// 删除
ListDelete(&L, 2, &e);
printf("删除的元素: %d\n", e); // 输出: 20
PrintList(L); // 输出: 10 25 30 40 50
// 查找
LNode *p = GetElem(L, 3);
printf("第3个元素: %d\n", p->data); // 输出: 30
printf("元素30的位序: %d\n", LocateElem(L, 30)); // 输出: 3
// 判空
printf("是否为空: %s\n", ListEmpty(L) ? "是" : "否");
// 销毁
free(L);
return 0;
}
四、循环单链表的进阶操作
4.1 判断是否为循环链表
/* 判断链表是否为循环链表(而非普通单链表)
* 思路:从任意结点出发,沿next指针遍历,若能回到起点则为循环链表
*/
bool IsCircular(LNode *L) {
if (L == NULL) return false;
LNode *slow = L;
LNode *fast = L;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) // 快慢指针相遇
return true;
}
return false;
}
4.2 合并两个循环单链表
/* 将两个循环单链表La和Lb合并为一个
* 思路:找到La的尾结点a,Lb的尾结点b
* 将a->next指向Lb的头结点,Lb->next指向La的首元素
* 释放Lb的头结点
*/
CircularLinkList MergeList(CircularLinkList *La, CircularLinkList *Lb) {
if (*La == NULL || *Lb == NULL) return NULL;
// 找到La的尾结点
LNode *pa = *La;
while (pa->next != *La) // next指向头结点时为尾结点
pa = pa->next;
// 找到Lb的尾结点
LNode *pb = *Lb;
while (pb->next != *Lb)
pb = pb->next;
// 连接两个环
pa->next = (*Lb)->next; // La尾结点指向Lb首元素
pb->next = *La; // Lb尾结点指向La头结点
// 释放Lb的头结点(若Lb也有头结点)
free(*Lb);
return *La;
}
4.3 循环单链表的逆置
/* 逆置循环单链表(原地逆置)
* 思路:每次取下一个结点,头插到结果链表中
* 注意:逆置后不再是循环链表(因为尾结点next应指向NULL)
*/
void Reverse(CircularLinkList *L) {
if (*L == NULL || (*L)->next == *L) return; // 空表或只有一个头结点
LNode *p = (*L)->next; // p指向第一个数据结点
(*L)->next = *L; // 断开,头结点指向自己
LNode *r;
while (p != *L) { // 依次取下每个结点
r = p->next;
p->next = (*L)->next;
(*L)->next = p;
p = r;
}
// 逆置后为普通单链表,尾结点需要手动处理
// 找到尾结点并使其next=NULL
LNode *tail = (*L)->next;
while (tail->next != *L)
tail = tail->next;
tail->next = NULL; // 逆置后不再是循环链表
}
4.4 约瑟夫环问题
约瑟夫环是循环链表的经典应用:
问题:n 个人围成一圈,从第 k 个人开始报数,报到 m 的人出圈,求最后剩下的人。
/* 约瑟夫问题
* n: 总人数, k: 起始位置, m: 报数上限
*/
int Josephus(int n, int k, int m) {
if (n < 1 || k < 1 || m < 1) return -1;
// 创建循环链表
CircularLinkList L;
InitList(&L);
LNode *r = L;
for (int i = 1; i <= n; i++) {
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = i;
s->next = r->next;
r->next = s;
r = s;
}
// 找到第k个人
LNode *p = L;
for (int i = 0; i < k - 1; i++)
p = p->next;
// 开始报数
while (p->next != p) { // 剩一个人时停止
for (int i = 1; i < m - 1; i++) // 报m-1次,移动m-1次
p = p->next;
// 第m个人出圈
LNode *q = p->next; // q指向第m个结点
printf("%d 出圈\n", q->data);
p->next = q->next;
free(q);
p = p->next; // 从下一个人继续
}
int result = p->data;
free(p);
return result;
}
五、时间与空间复杂度分析
5.1 时间复杂度
| 操作 | 最好情况 | 最坏情况 | 平均情况 | 说明 |
|---|---|---|---|---|
| 按位查找 GetElem | O(1) | O(n) | O(n) | 需遍历链表 |
| 按值查找 LocateElem | O(1) | O(n) | O(n) | 目标在起始位置最好 |
| 插入(按位) | O(1) | O(n) | O(n) | 需先查找位置 |
| 删除(按位) | O(1) | O(n) | O(n) | 需先查找位置 |
| 尾插法建表 | O(1)/次 | O(1)/次 | O(1)/次 | 无需遍历 |
| 头插法建表 | O(1)/次 | O(1)/次 | O(1)/次 | 每次O(1),共n次 |
| 求表长 | O(n) | O(n) | O(n) | 需遍历计数 |
| 合并两表 | O(n) | O(n) | O(n) | 找尾结点 |
5.2 空间复杂度
| 类型 | 空间复杂度 | 说明 |
|---|---|---|
| 循环单链表 | O(n) | n个结点 |
| 各操作辅助空间 | O(1) | 仅需常数个临时指针 |
5.3 与普通单链表时间复杂度对比
| 操作 | 普通单链表 | 循环单链表 |
|---|---|---|
| 按位查找 | O(n) | O(n) |
| 尾插法建表 | O(n²)(每次需遍历找尾) | O(n)(无需遍历) |
| 合并两表 | O(m+n)(需遍历找尾) | O(m+n)(无需遍历) |
| 求表长 | O(n) | O(n) |
📌 考研重点:循环单链表在尾插法和合并操作上具有时间优势,因为无需遍历找尾结点。
六、优缺点对比
6.1 循环单链表优点
- 首尾连接:从任意结点可遍历全表,遍历更加灵活
- 尾操作高效:尾插、合并等操作无需遍历找尾,O(1) 时间
- 内存利用率高:没有 NULL 指针空间浪费
- 适合环形结构:天然适合约瑟夫环等环形问题
6.2 循环单链表缺点
- 无法通过 NULL 判断结束:需要额外变量记录表长或判断是否回到起点
- 容易死循环:如果遍历终止条件写错,会陷入无限循环
- 删除尾结点仍需遍历:虽然尾插快,但删除尾结点仍需找到前驱
- 实现细节多:边界条件(空表、只有一个结点等)处理复杂
七、与普通单链表的对比
| 对比维度 | 普通单链表 | 循环单链表 |
|---|---|---|
| 尾结点的 next | NULL | 指向头结点(或首结点) |
| 遍历终止条件 | p != NULL | p != L(头结点) |
| 空表判断(带头) | L->next == NULL | L->next == L |
| 从尾结点到首元素 | 无法直接访问 | 可直接访问 |
| 头插法 | O(n)(需在表头插入) | O(1)(无需额外处理) |
| 尾插法 | O(n)(需遍历找尾) | O(1) |
| 合并操作 | O(m+n) | O(m+n)(但常数更小) |
| 约瑟夫环问题 | 不适合 | 天然适合 |
| 代码复杂度 | 较低 | 稍高(边界条件多) |
八、应用场景
8.1 适合使用循环单链表的场景
- 约瑟夫环问题:n人围圈报数出列
- 时间片轮转调度:操作系统进程调度
- LRU 缓存淘汰:循环结构便于热点数据管理
- 多项式表示:多项式加法等运算
- 音乐播放列表:播放列表循环播放
- 缓存缓冲区:循环队列的链表实现
8.2 实际应用举例
| 应用 | 使用原因 |
|---|---|
| 约瑟夫环 | 天然环形结构,无需额外边界处理 |
| 操作系统进程调度 | 循环遍历就绪队列,时间片均匀分配 |
| 扑克牌游戏 | 循环洗牌、发牌过程 |
| 内存管理(Buddy系统) | 循环链表管理空闲块 |
| 浏览器"最近关闭"标签 | 可循环浏览历史 |
九、高频考点与易错点
9.1 高频考点
考点1:空表判断 ⭐⭐⭐
// 循环单链表(带头结点)空表判断
L->next == L; // ✅ 正确:头结点指向自己
// ❌ 错误:混淆普通单链表的判断方式
L->next == NULL; // ❌ 这是普通单链表的判断!
考点2:遍历终止条件 ⭐⭐⭐
// 普通单链表遍历
while (p != NULL) { p = p->next; }
// 循环单链表遍历(带头结点)
while (p != L) { p = p->next; } // ✅ 回到头结点终止
// ❌ 错误:循环单链表用NULL判断会死循环
while (p->next != NULL) { } // ❌ 会死循环!
考点3:循环单链表与普通单链表代码差异 ⭐⭐⭐
| 操作 | 普通单链表 | 循环单链表 |
|---|---|---|
| 空表判断 | L->next == NULL | L->next == L |
| 遍历终止 | p != NULL | p != L |
| 尾插法 | 需遍历找尾 O(n) | 无需遍历 O(1) |
| 插入到尾部 | 需遍历 | 直接插 |
考点4:循环链表合并 ⭐⭐⭐
两个循环链表合并的关键步骤:
- 找 La 的尾结点(
pa->next == La) - 找 Lb 的尾结点(
pb->next == Lb) pa->next = Lb->next(La尾连Lb首)pb->next = La(Lb尾连La头)
考点5:约瑟夫环 ⭐⭐⭐
约瑟夫环问题的核心:
- 使用循环链表模拟环形结构
- 报数 m-1 次后,删除第 m 个人
- 循环直到只剩一人
9.2 易错点汇总
易错点1:死循环(最常见!)
// ❌ 错误:使用NULL作为循环终止条件,会死循环!
void PrintList_Wrong(CircularLinkList L) {
LNode *p = L->next;
while (p != NULL) { // 永远到不了NULL!
printf("%d ", p->data);
p = p->next;
}
}
// ✅ 正确:使用头结点作为终止条件
void PrintList_Right(CircularLinkList L) {
LNode *p = L->next;
while (p != L) { // 回到头结点时终止
printf("%d ", p->data);
p = p->next;
}
}
易错点2:混淆空表判断
// ❌ 错误:循环单链表空表判断用NULL
if (L == NULL) // ❌ 这是不带头结点空表的判断!
// ✅ 正确:带头结点的循环单链表空表
if (L->next == L) // ✅ 头结点指向自己
// ✅ 正确:不带头结点的循环单链表空表
if (L == NULL) // ✅
易错点3:尾插法忘记更新尾指针关系
// ❌ 错误:尾插法未连接成环
s->next = NULL; // ❌ 错误!循环链表尾结点next应指向头
r->next = s;
r = s;
// ✅ 正确:尾插法保证循环结构
s->next = r->next; // ✅ 新结点指向头结点
r->next = s; // ✅ 原尾结点指向新结点
r = s; // ✅ 移动尾指针
易错点4:插入时忘记处理循环关系
// ❌ 错误:在任意位置插入后,未检查是否破坏循环结构
bool ListInsert_Wrong(CircularLinkList *L, int i, ElemType e) {
LNode *p = GetElem(*L, i - 1);
if (p == NULL) return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next; // 对于循环链表,这可能导致问题
p->next = s;
return true;
// 若p是尾结点,这里未处理s->next应指向头
}
// ✅ 正确:通用插入(已有GetElem保证循环结构正确)
// 循环链表中,p->next 已经是循环的(指向首结点或头结点)
// 因此 s->next = p->next 已经处理了指向关系
易错点5:删除最后一个数据结点后未处理空表
// ❌ 错误:删除后头结点未正确回指自己
bool ListDelete_Wrong(CircularLinkList *L, int i, ElemType *e) {
LNode *p = GetElem(*L, i - 1);
if (p == NULL || p->next == NULL) return false; // ❌ 这个条件不对!
// ...
}
// ✅ 正确:判断是否删除了最后一个数据结点
bool ListDelete_Right(CircularLinkList *L, int i, ElemType *e) {
if (i < 1) return false;
LNode *p = GetElem(*L, i - 1);
if (p == NULL || p->next == L) return false; // ✅ p->next是头结点则越界
LNode *q = p->next;
*e = q->data;
p->next = q->next;
free(q);
return true;
}
易错点6:逆置后忘记处理尾结点
// 逆置后,原来的首结点变成尾结点
// 尾结点的next必须指向NULL(或头结点)
// 否则逆置后的链表仍然是循环的
tail->next = NULL; // ✅ 必须加上!
9.3 大题常见考法
- 选择题:循环链表与普通单链表的区别、空表判断
- 选择题:约瑟夫环问题分析
- 算法设计题:循环单链表的合并
- 算法设计题:约瑟夫环
- 算法设计题:判断链表是否有环
- 填空题:各操作的终止条件、指针域的设置
十、真题演练
题目1(选择题)
题目:在循环单链表中,能根据某结点的指针域直接找到其后继的时间复杂度为( )。
选项:A. O(1) B. O(n) C. O(n²) D. O(log₂n)
答案:A
解析:循环单链表是单链表的变体,其后继指针操作与普通单链表完全一致,都是 O(1) 时间。
题目2(选择题)
题目:设有一个循环单链表的长度为 n,若在表头插入一个新结点,其时间复杂度为( )。
选项:A. O(1) B. O(n) C. O(n²) D. O(nlogn)
答案:A
解析:在表头插入,只需要修改头结点的 next 指针指向新结点,新结点的 next 指向原首元素。循环单链表的尾结点已经指向头结点,无需修改。O(1) 时间。
题目3(算法设计)
题目:编写算法,合并两个循环单链表 La 和 Lb(均为带头结点的循环单链表),结果放在 La 中。
void MergeList(CircularLinkList *La, CircularLinkList *Lb) {
if (*La == NULL || *Lb == NULL || (*La)->next == *La) {
// La为空表,直接返回Lb(或将Lb赋值给La)
return;
}
if ((*Lb)->next == *Lb) {
// Lb为空表,无需合并
return;
}
// 找La的尾结点:next指向头结点即为尾
LNode *pa = *La;
while (pa->next != *La)
pa = pa->next;
// 找Lb的尾结点
LNode *pb = *Lb;
while (pb->next != *Lb)
pb = pb->next;
// 连接两个环
pa->next = (*Lb)->next; // La尾结点指向Lb首元素
pb->next = *La; // Lb尾结点指向La头结点
free(*Lb); // 释放Lb的头结点
}
复杂度分析:时间 O(m+n)(找两个尾结点),空间 O(1)。
题目4(算法设计)
题目:编写算法,判断一个单链表是否为循环链表(不带头结点)。
/* 判断链表是否为循环链表
* 思路:使用快慢指针,若快指针能追上慢指针则为循环链表
*/
bool IsCircular(LNode *head) {
if (head == NULL) return false;
LNode *slow = head;
LNode *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return true; // 快慢指针相遇,存在环
}
return false; // fast到达NULL,无环
}
复杂度分析:时间 O(n),空间 O(1)。
题目5(选择题)
题目:在带头结点的循环单链表中,设 p 为指向某结点的指针,则 p 指向尾结点的条件是( )。
A. p->next == NULL
B. p->next == p
C. p->next == L(L为头结点指针)
D. p == L
解析:循环单链表中,尾结点的 next 指向头结点 L,因此判断条件为 p->next == L。
答案:C
十一、总结速记
🔑 核心概念速记
【循环单链表的本质】
= 普通单链表 + 尾结点 next 指向头结点
= 形成逻辑上的"环"
= 无 NULL 指针,遍历以头结点为终止标志
【与普通单链表的两处关键差异】
① 空表判断:L->next == L(而非 L->next == NULL)
② 遍历终止:p != L(而非 p != NULL)
【尾插法核心代码】
s->next = r->next; // 新结点指向头结点
r->next = s; // 原尾结点指向新结点
r = s; // 移动尾指针
🔑 三个必背公式
空表判断(带头结点):L->next == L
遍历终止条件: p != L
尾结点判断: p->next == L
🔑 循环单链表 vs 普通单链表一句话
普通单链表:尾结点 next 为 NULL,遍历以 NULL 为终止,尾插需遍历找尾 O(n)
循环单链表:尾结点 next 指向头结点,遍历以头结点为终止,尾插无需遍历 O(1)
🔑 时间复杂度速记
按位/按值查找:O(n)
头插/尾插(每次):O(1) ← 循环单链表尾插优势
按位插入/删除:O(n)(含查找时间)
合并两表:O(m+n)(找两个尾结点)
求表长:O(n)
🔑 考试答题模板
写循环单链表算法时,必须包含:
- ✅ 初始化:
(*L)->next = *L(头结点指向自己) - ✅ 遍历终止:
p != L(不能用p != NULL) - ✅ 空表判断:
L->next == L - ✅ 尾插法:
s->next = r->next; r->next = s; r = s; - ✅ 尾结点判断:
p->next == L
🔑 知识体系框架
循环单链表(Circular Singly Linked List)
│
├── 结构特征
│ ├── 尾结点 next → 头结点(形成环)
│ ├── 无 NULL 指针
│ └── 带头结点(考研默认)
│
├── 关键判断
│ ├── 空表:L->next == L
│ ├── 遍历终止:p != L
│ └── 尾结点:p->next == L
│
├── 核心优势
│ ├── 尾插法:O(1)(无需遍历找尾)
│ ├── 合并两表:O(m+n)(常数更小)
│ └── 从任意结点可遍历全表
│
└── 应用
├── 约瑟夫环(经典应用)
├── 操作系统时间片轮转调度
├── 音乐播放列表循环播放
└── 循环缓冲区管理
📚 参考资料
- 王道考研《数据结构》第2章 线性表
- 严蔚敏《数据结构(C语言版)》第2章
- 2027年408考试大纲·数据结构部分
本文涵盖2027年408考研数据结构循环单链表全部考点,建议结合真题反复练习代码实现。
如有疑问,欢迎在评论区交流!


被折叠的 条评论
为什么被折叠?



