循环单链表的实现_2027考研408

循环单链表的实现——2027考研408数据结构全攻略

适用范围:2027年全国硕士研究生招生考试·计算机学科专业基础(408)
考纲章节:第二章 线性表 → 2.3 线性表的链式表示 → 循环链表
难度等级:⭐⭐⭐⭐(进阶重点,理解记忆型考点多)


目录

  1. 知识点概念
  2. 循环单链表的结构定义
  3. 循环单链表的实现(基本操作)
  4. 循环单链表的进阶操作
  5. 时间与空间复杂度分析
  6. 优缺点对比
  7. 与普通单链表的对比
  8. 应用场景
  9. 高频考点与易错点
  10. 真题演练
  11. 总结速记

一、知识点概念

1.1 循环链表定义

循环链表(Circular Linked List) 是链表的另一种形式,其特点是将链表尾结点的指针域由 NULL 改为指向链表的头结点(或第一个结点),使链表形成一个环。

循环链表分为两类:

  • 循环单链表:由单链表首尾相连构成
  • 循环双链表:由双链表首尾相连构成

本章重点讲解循环单链表

1.2 循环单链表定义

循环单链表(Circular Singly Linked List) 是在单链表的基础上,将最后一个结点的 next 指针指向头结点(或第一个结点),形成一个环形结构。

普通单链表

[头] → [a1] → [a2] → [a3] → NULL

循环单链表

[头] ────────────────────┐
  ↓                       │
[a1] → [a2] → [a3] ──────┘

1.3 核心特征

  1. 无 NULL 指针:遍历循环链表时,没有 NULL 作为终止条件,必须以某个结点的 next 再次指向头结点作为循环终止条件。
  2. 从任意结点出发可遍历全表:不像普通单链表只能从头结点出发。
  3. 首尾相连:尾结点的 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 时间复杂度

操作最好情况最坏情况平均情况说明
按位查找 GetElemO(1)O(n)O(n)需遍历链表
按值查找 LocateElemO(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 循环单链表优点

  1. 首尾连接:从任意结点可遍历全表,遍历更加灵活
  2. 尾操作高效:尾插、合并等操作无需遍历找尾,O(1) 时间
  3. 内存利用率高:没有 NULL 指针空间浪费
  4. 适合环形结构:天然适合约瑟夫环等环形问题

6.2 循环单链表缺点

  1. 无法通过 NULL 判断结束:需要额外变量记录表长或判断是否回到起点
  2. 容易死循环:如果遍历终止条件写错,会陷入无限循环
  3. 删除尾结点仍需遍历:虽然尾插快,但删除尾结点仍需找到前驱
  4. 实现细节多:边界条件(空表、只有一个结点等)处理复杂

七、与普通单链表的对比

对比维度普通单链表循环单链表
尾结点的 nextNULL指向头结点(或首结点)
遍历终止条件p != NULLp != L(头结点)
空表判断(带头)L->next == NULLL->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 == NULLL->next == L
遍历终止p != NULLp != L
尾插法需遍历找尾 O(n)无需遍历 O(1)
插入到尾部需遍历直接插
考点4:循环链表合并 ⭐⭐⭐

两个循环链表合并的关键步骤:

  1. 找 La 的尾结点(pa->next == La
  2. 找 Lb 的尾结点(pb->next == Lb
  3. pa->next = Lb->next(La尾连Lb首)
  4. 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. 选择题:循环链表与普通单链表的区别、空表判断
  2. 选择题:约瑟夫环问题分析
  3. 算法设计题:循环单链表的合并
  4. 算法设计题:约瑟夫环
  5. 算法设计题:判断链表是否有环
  6. 填空题:各操作的终止条件、指针域的设置

十、真题演练

题目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)

🔑 考试答题模板

写循环单链表算法时,必须包含:

  1. ✅ 初始化:(*L)->next = *L(头结点指向自己)
  2. ✅ 遍历终止:p != L(不能用 p != NULL
  3. ✅ 空表判断:L->next == L
  4. ✅ 尾插法:s->next = r->next; r->next = s; r = s;
  5. ✅ 尾结点判断: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考研数据结构循环单链表全部考点,建议结合真题反复练习代码实现。
如有疑问,欢迎在评论区交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值