C语言countoff是什么?怎么用?

99ANYc3cd6
预计阅读时长 24 分钟
位置: 首页 C语言 正文

这是一个非常经典的算法问题,通常用来模拟一种淘汰游戏。

C语言countoff
(图片来源网络,侵删)

问题定义

Countoff / 约瑟夫问题

有 n 个人围成一圈,从某个指定的人开始报数,数到 k 的那个人就被淘汰,然后从被淘汰的人的下一个人重新开始报数,数到 k 的 next person 再被淘汰,如此重复,直到所有人都被淘汰,求出这 n 个人被淘汰的顺序。

简单例子: 假设有 5 个人(编号 1, 2, 3, 4, 5),从第 1 个人开始报数,数到 2 的人被淘汰。 淘汰过程如下:

  1. 第一轮: 1, 2 (淘汰),剩下 [1, 3, 4, 5]。
  2. 第二轮: 从 3 开始,3, 4 (淘汰),剩下 [1, 3, 5]。
  3. 第三轮: 从 5 开始,5, 1 (淘汰),剩下 [3, 5]。
  4. 第四轮: 从 3 开始,3, 5 (淘汰)。
  5. 第五轮: 只剩下 3,3 被淘汰。

最终淘汰顺序是:2, 4, 1, 5, 3

C语言countoff
(图片来源网络,侵删)

解决思路

解决这个问题的关键在于如何模拟“围成一圈”以及如何找到下一个要被淘汰的人

主要有两种经典的解决方法:

  1. 模拟法(直观,易于理解):使用数据结构(如循环链表)来模拟整个淘汰过程。
  2. 数学递推法(高效,算法思想强):通过数学公式推导出最终结果,时间复杂度更低。

方法一:模拟法(使用循环链表)

这种方法最符合问题的直观描述。

算法思路

  1. 创建数据结构:使用一个循环链表来表示围成一圈的人,每个节点存储一个人的编号,并且最后一个节点的指针指向头节点,形成一个环。
  2. 初始化:创建一个包含 n 个节点的循环链表,节点值为 1 到 n。
  3. 模拟过程: a. 设置一个指针 current 指向链表的起始位置(比如编号为1的人)。 b. 设置一个计数器 count。 c. 循环进行,直到链表为空: i. 从 current 开始移动指针,count 递增。 ii. 当 count 等于 k-1 时,说明下一个节点就是要被淘汰的人。 iii. 删除这个节点,并将 current 指针移动到被删除节点的下一个节点(即新的起始点)。 iv. 重置 count 为 0,开始下一轮淘汰。

C语言代码实现

#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct Node {
    int number;
    struct Node* next;
} Node;
// 创建循环链表
Node* createCircularList(int n) {
    if (n <= 0) return NULL;
    Node *head = (Node*)malloc(sizeof(Node));
    head->number = 1;
    Node *current = head;
    for (int i = 2; i <= n; i++) {
        Node *newNode = (Node*)malloc(sizeof(Node));
        newNode->number = i;
        current->next = newNode;
        current = newNode;
    }
    // 将链表首尾相连,形成循环
    current->next = head;
    return head;
}
// 模拟淘汰过程并打印顺序
void countoff(int n, int k) {
    if (n <= 0 || k <= 0) {
        printf("Invalid input: n and k must be positive.\n");
        return;
    }
    Node *head = createCircularList(n);
    Node *current = head;
    Node *prev = NULL;
    // 找到尾节点,方便删除
    Node *tail = head;
    while (tail->next != head) {
        tail = tail->next;
    }
    int remaining = n;
    while (current->next != current) { // 当不止一个节点时
        // 数 k-1 个人
        for (int i = 1; i < k; i++) {
            prev = current;
            current = current->next;
        }
        // 删除 current 节点
        printf("%d ", current->number);
        prev->next = current->next;
        Node *toDelete = current;
        current = current->next; // 从下一个人重新开始
        free(toDelete);
        remaining--;
    }
    // 打印最后一个被淘汰的人
    printf("%d\n", current->number);
    free(current); // 释放最后一个节点
}
int main() {
    int n, k;
    printf("请输入总人数 n: ");
    scanf("%d", &n);
    printf("请输入报数 k: ");
    scanf("%d", &k);
    printf("淘汰顺序为: ");
    countoff(n, k);
    return 0;
}

代码分析

  • Node 结构体:定义了链表的节点,包含 number(编号)和 next(指向下一个节点的指针)。
  • createCircularList:创建一个 1n 的单向链表,然后将尾节点的 next 指针指向头节点,形成循环。
  • countoff
    • prev 指针总是指向 current 的前一个节点,这是为了方便删除 current
    • for (int i = 1; i < k; i++) 这个循环的作用是“走 k-1 步”,这样 current 就指向了第 k 个人。
    • prev->next = current->next; 这句是标准的链表删除操作,将前一个节点跳过当前节点,直接指向当前节点的下一个节点。
    • free(toDelete); 释放被淘汰节点的内存,防止内存泄漏。
  • 时间复杂度:O(n * k),外层循环执行 n 次,内层循环最多执行 k 次,当 k 接近 n 时,效率较低。

方法二:数学递推法(高效)

这种方法不模拟过程,而是直接计算出结果,效率极高。

C语言countoff
(图片来源网络,侵删)

算法思路

这个问题有一个著名的数学递推公式,通常被称为约瑟夫环公式

我们定义 f(n, k)n 个人、报数 k 时,最后剩下的人的初始编号

  • 基本情况

    • n = 1 时,圈里只有一个人,他肯定是最后剩下的。f(1, k) = 0 (如果编号从0开始) 或 f(1, k) = 1 (如果编号从1开始),我们这里用从0开始的编号,方便公式推导。
  • 递推关系: 假设我们已经知道 f(n-1, k) 的值,如何求出 f(n, k)

    1. 在第一轮淘汰中,第 k % n 个人(编号为 k-1)被淘汰。
    2. 淘汰之后,剩下 n-1 个人,新的起始点是原来编号为 k 的人。
    3. 问题变成了一个规模为 n-1 的新问题,在这个新问题中,原来编号为 k 的人相当于新编号的 0 号。
    4. 假设我们已经算出了这个新问题的解 f(n-1, k),这个解是新编号中的,我们需要把它转换回原来的编号。
    5. 转换公式是:原编号 = (新编号 + 起始点) % n
    6. 这里的起始点是 kf(n, k) = (f(n-1, k) + k) % n

C语言代码实现

#include <stdio.h>
// 递归实现(效率不高,容易栈溢出)
int josephus_recursive(int n, int k) {
    if (n == 1) {
        return 0; // 返回从0开始的编号
    }
    return (josephus_recursive(n - 1, k) + k) % n;
}
// 迭代实现(推荐,高效且不会栈溢出)
int josephus_iterative(int n, int k) {
    int result = 0; // f(1, k) = 0
    for (int i = 2; i <= n; i++) {
        result = (result + k) % i;
    }
    return result;
}
// 打印完整的淘汰顺序
void print_countoff_order(int n, int k) {
    if (n <= 0 || k <= 0) return;
    int *order = (int*)malloc(n * sizeof(int));
    int *people = (int*)malloc(n * sizeof(int));
    // 初始化人员列表 (0-based)
    for (int i = 0; i < n; i++) {
        people[i] = i;
    }
    int current_pos = 0;
    int remaining = n;
    for (int i = 0; i < n; i++) {
        // 找到下一个要被淘汰的人
        current_pos = (current_pos + k - 1) % remaining;
        // 记录淘汰顺序
        order[i] = people[current_pos];
        // 从数组中移除被淘汰的人
        for (int j = current_pos; j < remaining - 1; j++) {
            people[j] = people[j + 1];
        }
        remaining--;
    }
    // 打印结果 (转换为1-based)
    printf("淘汰顺序为: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", order[i] + 1);
    }
    printf("\n");
    free(order);
    free(people);
}
int main() {
    int n, k;
    printf("请输入总人数 n: ");
    scanf("%d", &n);
    printf("请输入报数 k: ");
    scanf("%d", &k);
    // 1. 找出最后剩下的人
    int last_survivor_0_based = josephus_iterative(n, k);
    int last_survivor_1_based = last_survivor_0_based + 1;
    printf("\n最后剩下的人是: %d\n", last_survivor_1_based);
    // 2. 打印完整的淘汰顺序
    printf("\n");
    print_countoff_order(n, k);
    return 0;
}

代码分析

  • josephus_recursive:直接实现了递推公式 f(n, k) = (f(n-1, k) + k) % n,虽然代码简洁,但当 n 很大时(n=10000),会导致递归深度过大,栈溢出。
  • josephus_iterative:用循环代替递归,从 i=2 开始,逐步计算 f(i, k),直到 i=n,这是解决该问题的最高效方法,时间复杂度为 O(n)
  • print_countoff_order:这个函数展示了如何用数学思想来模拟过程,以得到完整的淘汰顺序。
    • current_pos = (current_pos + k - 1) % remaining; 这句是核心,它模拟了在 remaining 个人中,从 current_pos 开始数 k-1 步,找到下一个被淘汰的人的位置。
    • 之后通过数组元素前移来模拟“淘汰”操作。

两种方法的比较

特性 模拟法(循环链表) 数学递推法
直观性 ,直接模拟了游戏过程,容易理解。 ,需要理解数学推导过程。
时间复杂度 O(n * k) O(n)
空间复杂度 O(n) (需要链表/数组) O(1) (迭代法) / O(n) (递归法栈空间)
适用场景 当 n 和 k 都比较小,或者需要模拟详细过程时。 当 n 非常大时,或者只关心最后剩下的人时。
功能 容易实现打印完整淘汰顺序 容易实现计算最后剩下的人,打印完整顺序需要额外逻辑。
  • Countoff(约瑟夫问题)是一个经典的算法题。
  • 模拟法使用循环链表,直观但效率较低,适合初学者理解。
  • 数学递推法非常高效,体现了算法的数学之美,是解决此类问题的最优方法。
  • 在实际面试或编程中,如果要求高效,应该优先选择数学迭代法,如果要求清晰易懂或者需要输出完整过程,模拟法也是一个不错的选择。
-- 展开阅读全文 --
头像
dede如何批量删除所有文章?
« 上一篇 04-16
dede前台首页空白,原因何在?
下一篇 » 04-16

相关文章

取消
微信二维码
支付宝二维码

目录[+]