4. 循环链表的约瑟夫环模拟

循环链表的约瑟夫环模拟 学习卡片

约瑟夫环的链表模型

约瑟夫环问题可以描述为:n 个人围成一圈,从第 1 个人开始报数,每报到第 m 个人就出列,然后从下一个人重新从 1 开始报数,直到所有人出列。用循环链表模拟时,每个节点代表一个人,节点的 next 指针始终指向环中的下一个存活节点。核心操作有两个:报数推进(沿 next 移动 m-1 次)和节点删除(让前驱节点的 next 跳过被删除节点)。

n=7, m=3 的约瑟夫环出列过程 初始环(7人) 1 2 3 4 5 6 7 出列顺序(每步删除一个节点) 第1轮:从1报数,3出列 → 环变为 1→2→4→5→6→7→1 第2轮:从4报数,6出列 → 环变为 1→2→4→5→7→1 第3轮:从7报数,2出列 → 环变为 1→4→5→7→1 第4轮:从4报数,7出列 → 环变为 1→4→5→1 第5轮:从1报数,5出列 → 环变为 1→4→1 第6轮:从1报数,1出列 → 环变为 4→4 第7轮:4出列 → 环空,最终幸存者:4 删除节点操作 prev → 被删节点的前驱 curr → 被删节点 prev->next = curr->next free(curr) 注意:若删头节点需更新 head

循环链表节点定义与建环

每个节点包含数据域和指针域。建环时先创建 n 个节点并依次链接,最后将尾节点的 next 指回头节点,形成闭环。头指针 head 指向第 1 个节点,同时用 tail 记录尾节点以便快速完成闭环。

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int id;                 /* 编号 1..n */
    struct Node *next;
} Node;

/* 创建 n 个节点的循环链表,返回头指针 */
Node *createCircle(int n) {
    if (n <= 0) return NULL;
    Node *head = (Node *)malloc(sizeof(Node));
    head->id = 1;
    head->next = head;      /* 先自环,便于统一处理 */
    Node *tail = head;
    for (int i = 2; i <= n; i++) {
        Node *p = (Node *)malloc(sizeof(Node));
        p->id = i;
        p->next = head;     /* 新节点指向头,保持环 */
        tail->next = p;     /* 旧尾指向新节点 */
        tail = p;           /* 更新尾 */
    }
    return head;
}

约瑟夫环模拟函数

模拟过程维护两个指针:prev 指向当前报数起点的前驱,curr 指向当前报数起点。每轮先让 curr 沿环前进 m-1 步(prev 同步跟随),此时 curr 就是出列者。删除时执行 prev->next = curr->next,然后释放 curr,并将 curr 重置为 prev->next 作为下一轮起点。当环中只剩一个节点时(curr->next == curr),该节点即为幸存者。

/* 返回最后幸存者的编号 */
int josephus(int n, int m) {
    if (n <= 0 || m <= 0) return -1;
    Node *head = createCircle(n);
    Node *prev = head;
    while (prev->next != head) prev = prev->next;  /* prev 指向尾节点 */
    Node *curr = head;

    while (curr->next != curr) {          /* 环中多于 1 个节点 */
        for (int i = 1; i < m; i++) {     /* 报数 m-1 次 */
            prev = curr;
            curr = curr->next;
        }
        printf("出列: %d\n", curr->id);
        prev->next = curr->next;          /* 跳过 curr,维护环 */
        free(curr);
        curr = prev->next;                /* 下一轮从下一个人开始 */
    }
    int survivor = curr->id;
    free(curr);
    return survivor;
}

int main(void) {
    int n = 7, m = 3;
    int s = josephus(n, m);
    printf("n=%d, m=%d 的幸存者: %d\n", n, m, s);
    return 0;
}

运行结果与验证

以 n=7、m=3 为例,程序输出如下(与上方 SVG 中的出列顺序一致):

出列: 3
出列: 6
出列: 2
出列: 7
出列: 5
出列: 1
n=7, m=3 的幸存者: 4
验证要点:出列顺序必须与手算一致;每删除一个节点后,prev->next 必须仍指向环中下一个存活节点;当 curr->next == curr 时循环终止,此时 curr 就是幸存者。若 m=1,则每轮都删除当前节点,出列顺序为 1,2,3,...,n,幸存者为 n。
常见错误:删除头节点时忘记更新 head 指针;报数循环写成 i <= m 导致多走一步;删除后未将 curr 重置为 prev->next,导致访问已释放内存。建议在每轮删除后打印当前环状态进行调试。