约瑟夫环问题详解

我们先看一下对该问题的描述:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。

解决该问题通常有两种方法:


  • 模拟法(模拟整个游戏的运行过程)
    • 循环链表
    • 数组
  • 数学公式法(直接通过公式推导得出结果,不关心具体过程)

谈不上哪种更简单,只在于思考问题、解决问题的方式不同而已。


  • 模拟法的时间复杂度为O(mn),当n和m很大时,程序的将很难在短时间内得到结果。
    • 模拟法的一个优点是:程序的设计思路很清晰。
  • 数学公式法的时间复杂度为O(n)。
    • 数学方法虽然使最终的程序编写起来很简单,但前提是,你得有足够强的抽象思维能力,能够得出最终的公式。

下面我们给出以上各种方法的实现:

循环链表模拟

确切说来,我们具体使用的是单向循环链表。我们从1,2,3,…n给每个人编号。一旦他出列,我们就将他从链表中删去,直到剩下最后一个人,即为获胜者。

typedef struct node {
    int number;     /*  编号  */
    struct node *next;
} Node;

这里需要注意的是删除操作。我们都知道,每个节点都有一个next指针指向它,为了将删除所有位置的节点看作一种通用的情况,我们需要一个指向next域的指针,即一个二级指针。而且在删除过程中,要注意保证链表不断裂。

int main(void)
{
    Node *head, **np, *tmp;
    int n, m, k;

    scanf("%d%d%d", &n, &m, &k);

    head = NULL;
    if ((head = creat_cll(head, n)) == NULL)
        return 1;    /*  初始化链表  */
    for (np = &head->next; *np != head; np = &(*np)->next)
        if ((*np)->number == k)    /*  找到开始报数的人  */
            break;
    k = 1;
    while ((*np)->next != *np) {    /*  剩下最后一个节点,循环终止  */
        if (k++ == m) {     /*  删除应该出列的人  */
            tmp = (*np)->next;
            free(*np);
            *np = tmp;
            k = 1;
        } else
            np = &(*np)->next;
    }
    printf("%d\n", (*np)->number);   /*  打印出获胜者  */
    return 0;
}

下面的操作类似于单链表的创建,只是最后我们需要把链表的首尾连接起来而已。

/*  creat_cll函数:创建一个单向循环链表,并初始化  */
Node *creat_cll(Node *head, int n)
{
    Node *new, *pre;

    if ((pre = new = head = malloc(sizeof(Node))) == NULL)
        return NULL;
    new->number = n;
    while (--n > 0) {
        if ((new = malloc(sizeof(Node))) == NULL)
            return NULL;
        new->next = pre;
        pre = new;
        new->number = n;
    }
    head->next = new;    /*  连接链表的首尾  */
    return head;
}

循环链表还有一种方法,就是采用惰性删除

意思就是我们并不真正地删除链表节点,而只是将它标记为已删除状态。因为我们的编号都是大于0的数,所以一旦某个人出列,我们只需将他的number置为0即可。

具体过程如下:

int main(void)
{
    Node *head, *p;
    int n, m, k;

    scanf("%d%d%d", &n, &m, &k);

    head = NULL;
    if ((head = creat_cll(head, n)) == NULL)
        return 1;
    for (p = head->next; p != head; p = p->next)
        if (p->number == k)    /*  找到开始报数的人  */
            break;
    k = 1;
    while (n > 1) {
        if (k++ == m) {
            p->number = 0;
            k = 1;
            n--;
        }
        do {    /*  每次遍历链表的时候,需要跳过那些已出列的节点,即number域为0的节点  */
            p = p->next;
        } while (!p->number);
    }
    printf("%d\n", p->number);
    return 0;
}

最后我们并没有销毁链表,你可以自行加上该操作。


数组模拟

数组模拟方法的难点在于我们要循环使用一个线性数组,其实稍微认真思考一下,也不是很难。每当遍历到数组末尾的时候,我们就从头重新开始遍历,一旦某个人出列,就将该处的值置为0,直到剩下最后一个人。

int main(void)
{
    char *p, *q, 
    char *q_end;    /*  指向数组末尾  */
    int n, m, k, i;

    scanf("%d%d%d", &n, &m, &k);
    if ((q = malloc(sizeof(int) * n)) == NULL)
        return 1;    /*  为使用的数组分配内存空间  */
    for (i = 0; i < n; i++)
        q[i] = i + 1;    /*  初始化数组  */

    i = 1;
    q_end = q + n;
    p = q + k - 1;     /*  指向开始报数的人  */
    while (n > 1) {
        if (i++ == m) {
            *p = 0;
            i = 1;
            n--;
        }
        do {
            if (++p >= q_end)    /*  到达数组末尾后,就从头重新开始遍历  */
                p = q;
        } while (!*p);    /*  跳过已出列的人  */
    }
    printf("%d\n", *p);
    free(q);
    q = NULL;
    return 0;
}

最后,我们给出数学公式法的解答

猜你喜欢

转载自blog.csdn.net/qq_41145192/article/details/80808099