链表家族收官篇:循环链表

一、填坑时刻:从"首尾相连"说起

上一篇博客的末尾,我留下了一个悬念:

"如果让最后一个节点的 next 指回头节点,形成一个环,又会诞生什么样的数据结构呢?"

今天,这个坑该填上了。

这种"首尾相接"的链表,就是我们要认识的链表家族最后一位成员------循环链表(Circular Linked List)

至此,链表家族的三大成员我们已经全部集齐:

类型 简称 核心特征
单链表 SLL 单向线性,尾节点指向 NULL
双向链表 DLL 双向线性,头尾各指向 NULL
循环链表 CLL 首尾相接,形成闭环

二、循环链表:没有终点的链表

普通链表(单链表、双向链表)都是"有头有尾"的------走到最后一个节点,再往前就是 NULL,意味着"此路不通"。

循环链表 做了一个大胆的改动:让最后一个节点不再指向 NULL,而是绕回来指向链表中的某个节点(通常是头节点)

于是,链表不再是"一条线",而变成了一个

单向循环链表

这是单链表的"循环版"。每个节点仍然只有一个 next 指针,但最后一个节点的 next 不再指向 NULL,而是指回头节点。

复制代码
┌─────────────────────────┐
↓ │
┌───┬───┐ ┌───┬───┐ ┌───┬───┐
│ 10│next│───→│ 20│next│───→│ 30│next│──┘
└───┴───┘ └───┴───┘ └───┴───┘
↑
head

在这个环里,你可以从 head 出发,沿着 next 一直走,最终又会走回 head。链表 没有真正的"末尾"

双向循环链表

这是双向链表的"循环版"。头节点的 prev 指向尾节点,尾节点的 next 指回头节点,形成一个可以双向无限循环的环。

复制代码
┌─────────────────────────────────────┐
↓ │
┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┐
┌──│prev│10 │next│←──│prev│20 │next│←──│prev│30 │next│──┐
│ └───┴───┴───┘ └───┴───┴───┘ └───┴───┴───┘ │
└─────────────────────────────────────────────────────┘

三、循环链表 vs 普通链表

特性 单/双向链表 循环链表
尾部指向 NULL 头节点(或前驱节点)
遍历终止条件 p == NULL p == head(绕了一圈回来)
是否有明确的"起点/终点" ✅ 有 ❌ 没有,只有相对的头节点
从尾到头 需要重新从头遍历 直接沿着环走即可
内存占用 相同 相同(只是指针指向不同)

关键区别 :普通链表靠 NULL 来判断"到头了",循环链表靠"是不是又回到了起点"来判断。


四、循环链表的核心操作

循环链表的操作和普通链表大同小异,但有几个细节需要特别注意。

1. 遍历:别陷入死循环

普通链表遍历的写法:

c 复制代码
while (p != NULL) { ... } // 走到 NULL 就停

循环链表如果这么写,就 永远停不下来了 (因为根本没有 NULL)。正确的写法是:

c 复制代码
if (head == NULL) return; // 空链表特判

Node* p = head;
do {
    // 处理当前节点
    printf("%d ", p->data);
    p = p->next;
} while (p != head); // 走回起点就停

do-while 是为了确保至少执行一次(因为一开始 p == head)。当然,用 while 配合 if 判断也可以。

2. 尾部插入:要同时照顾"新尾巴"和"头"

在单向循环链表中,往尾部插入一个新节点,需要做两件事:

  • 原尾节点的 next 指向新节点

  • 新节点的 next 指回 head

    插入前: ... → 30 → head(10)
    插入后: ... → 30 → 40 → head(10)

3. 只有一个节点时的特殊性

当循环链表里只有一个节点时,它的 next 指向自己

复制代码
┌────────┐
↓ │
┌───┬───┐ │
│ 10│next│───┘
└───┴───┘
↑
head

这时候删除这个唯一节点,要记得把 head 设为 NULL,否则链表虽然空了,但 head 还悬着一个野指针。


五、C语言实现:单向循环链表

下面实现一个支持尾部插入遍历打印的单向循环链表:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 1. 节点定义(和单链表一模一样)
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 2. 创建新节点
Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL; // 先默认指向 NULL,后面再调整
    return newNode;
}

// 3. 尾部插入
void append(Node** head, int data) {
    Node* newNode = createNode(data);

    // 情况1:链表为空
    if (*head == NULL) {
        *head = newNode;
        newNode->next = *head; // 自己指向自己,形成自环
        return;
    }

    // 情况2:链表非空,找到最后一个节点
    Node* tail = *head;
    while (tail->next != *head) { // 尾节点的 next 等于 head
        tail = tail->next;
    }

    // 接入新节点
    tail->next = newNode; // 原尾节点指向新节点
    newNode->next = *head; // 新尾节点指回头节点
}

// 4. 遍历打印循环链表
void printList(Node* head) {
    if (head == NULL) {
        printf("链表为空\n");
        return;
    }

    Node* p = head;
    printf("循环链表: ");
    do {
        printf("%d ", p->data);
        p = p->next;
    } while (p != head); // 回到起点就结束
    printf("(回到起点)\n");
}

// 5. 释放链表
void freeList(Node** head) {
    if (*head == NULL) return;

    Node* p = (*head)->next;
    while (p != *head) {
        Node* temp = p;
        p = p->next;
        free(temp);
    }
    free(*head); // 释放头节点
    *head = NULL; // 别忘了把 head 置空
}

// 测试
int main() {
    Node* head = NULL;

    append(&head, 10);
    append(&head, 20);
    append(&head, 30);

    printList(head); // 循环链表: 10 20 30 (回到起点)

    freeList(&head);
    return 0;
}

代码中的两个关键点

位置 代码 为什么
空链表插入 newNode->next = *head; 只有一个节点时,它自己指向自己
遍历 do { ... } while (p != head); 确保至少打印一次,且不会死循环

六、循环链表的经典应用

循环链表虽然看起来只是"把尾巴连回了头",但这个特性让它在特定场景中大放异彩:

1. 约瑟夫问题(Josephus Problem)

n 个人围成一圈,从第 k 个人开始报数,数到 m 的人出局,下一个人再从 1 开始报数......求最后剩下的人。

这个问题天然就是"围成一圈"的模型,用循环链表来模拟再合适不过------每个人是一个节点,出局就是删除节点,删完继续沿着 next 报数。

2. 操作系统的进程调度

操作系统中的进程调度算法(如时间片轮转调度)会把所有就绪进程排成一个循环队列。CPU 依次执行每个进程一个时间片,执行完就轮到下一个,周而复始。循环链表完美契合这种"轮流来、不停转"的逻辑。

3. 多人轮流游戏/轮播图

比如一款回合制游戏,玩家按顺序行动,最后一个玩家行动完后又回到第一个玩家。用循环链表存储玩家顺序,逻辑非常自然。


七、链表家族大总结

至此,我们已经走完了链表家族的完整版图:

复制代码
链表(Linked List)
│
┌───────┴───────┐
│               │
线性链表        循环链表
│               │
┌──┴──┐      ┌──┴──┐
↓     ↓      ↓     ↓
单链表 双向链表 单向循环 双向循环
(SLL) (DLL)  (SCLL)  (DCLL)
数据结构 一句话记忆
单链表 一条路走到黑
双向链表 能进能退,双向通行
循环链表 没有终点,周而复始

链表的世界看似复杂,但万变不离其宗:节点 + 指针 = 链表。理解了这一点,无论是单链、双链还是循环链,都只是在这个基础上做的"加减法"。

相关推荐
景熙55231 小时前
单体项目编写思路和落地形式
java·开发语言
库玛西2 小时前
哈夫曼树与前缀编码:数据压缩的贪心核心
c语言·c++·笔记·考研
计算机毕设定制辅导-无忧学长2 小时前
《基于SpringBoot的未成年人健康知识科普平台的设计与实现》
java·开发语言·vue.js·spring boot·未成年人健康知识科普平台
Ivanqhz2 小时前
Ping-Pong 双缓冲
开发语言·人工智能·python·深度学习·mlir
m0_734571762 小时前
深入理解C++ RAII
开发语言·c++
泡海椒2 小时前
jquick-pdf 表格实战:动态数据 PDF 报表生成
java·开发语言·pdf
雷✘2 小时前
C 语言预处理中的宏展开、条件编译与头文件保护
c语言
aramae2 小时前
模拟实现memset()(C语言)
c语言·开发语言·后端
wuyk5553 小时前
【Socket 进阶之路】第 7 章 IO 多路复用 select & poll 完整实战|多 fd 监听、超时等待、优缺点横向对比
服务器·开发语言·网络·数据库·物联网