约瑟夫环问题——用循环链表、队列、递归三种方法实现

约瑟夫问题描述

 编号为1 到 N 的 N 个人围坐在一起形成一个圆圈,从编号为 1 的人开始依次报数(1,2,3…依次报),数到 m 的 人会被杀死出列,之后的人再从 1 开始报数。直到最后剩下一人,求这个人的编号。

解题思路

循环链表

 对于这个问题,相信很多人首先想到的就是用循环链表。

基本思路:
  1. 首先构造一个循环链表,每个结点的初始值为1-n。
  2. 然后根据m的值来进行删除,每到第m个结点就执行删除操作。
  3. 最后剩下的一个结点的值即为所求。

实现代码:
#include<stdio.h>
#include<malloc.h>

typedef struct LNode{
    int data;
    struct LNode *next;
}LNode;

//初始化一个循环链表,data域为1-n
LNode *head,*rear;
LNode *Init(int n){
    head = (LNode *)malloc(sizeof(LNode));
    head->data = 1;
    rear = head;
    for(int i=2; i<=n; i++){
        LNode *q = (LNode *)malloc(sizeof(LNode));
        q->data = i;
        rear->next = q;
        rear = q;
    }
    rear->next = head;
    return head;
}

//删除操作
void Delete(LNode *p){
    LNode *q = p->next; //q为要删除的结点
    p->next = q->next;
    printf("%d ",q->data);   //如果只需要输出最后一个人的编号,则去掉这句即可
    free(q);
}

int main(){
    int m,n;
    scanf("%d%d",&n,&m);
    head = Init(n);
    LNode *p = head;
    while(p->next != p){  //循环直到只剩下一个结点
        for(int i=0; i<m-2; i++){ //用于找到报m-1的结点,然后执行删除操作
            p = p->next;
        }
        Delete(p);  //删除p->next
        p = p->next;
    }
    printf("%d\n",p->data); //最后输出剩下的唯一一个编号
    return 0;
}

  对于循环链表的实现方式,其时间复杂度为O(m*n),空间复杂度为O(n)。


队列

  对于队列,其解题思路与链表大致相同。其实很多人会想到用循环队列,其实没有必要,只需要用普通队列就可以实现。
基本思路:
  1. 首先,将编号1-n按序入队,形成初始队列。
  2. 对于每个编号,如果该编号不是当前循环的第m个,那么出队之后再次将其压入队列尾部,即出队后再入队,用queue来模拟循环队列的效果;否则,直接出队。
  3. 队列中剩下的最后一个编号即为所求。

 由于C++的STL中提供了队列模板可供使用,于是对于队列我采用了C++来编写,对于队列模板的基本用法,可以参考STL - queue的用法与实例

实现代码:

#include<iostream>
#include<queue>
using namespace std;

queue<int> node;

//初始化队列
void Init(int n){
    for(int i=1; i<=n; i++){
        node.push(i);
    }
}

//删除
void Delete(){
    int temp = node.front();
    cout<<temp<<" ";  //如果只输出最后一个编号,那么去掉本句即可
    node.pop();
}

int main(){
    int n,m;
    cin>>n>>m;
    Init(n);
    while(node.size()>1){
        for(int i=0; i<m-1; i++){  //如果不是第m个编号,那么先出队,再加入队尾
            int tmp = node.front();
            node.pop();
            node.push(tmp);
        }
        Delete();  //否则出队
    }
    cout<<node.front()<<endl;  //输出最后一个编号
    node.pop();  //队列清空
    return 0;
}

  对于队列的实现方式,其时间复杂度为O(m*n),空间复杂度为O(n)。


递归,用一行代码实现

  其实这道题还可以用递归来解决。基本思路是每次我们删除了某一个人之后,我们就对剩下这些人重新编号。那么,解决这个问题最大的难点就变成了找出删除前和删除后人编号的映射关系。
  为此,定义一个递归函数f(n, m),其返回结果是剩下的人的编号。显然,当n=1时,f(n, m) = 1。如果能找到f(n, m) 和 f(n-1, m)之间的关系,那么就可以用递归的方式来解决这个问题了。
 初始值: 1,2,3,… ,m-2,m-1,m,m+1,m+2,… ,n
 那么进行一次删除后,删除了编号为m的结点,然后对剩下的n-1个结点进行重新编号:

删除前old删除后new
m-2n-2
m-1n-1
m被删除了
m+11(下次从这里开始报数)
m+22
nn-m

 那么新的环中有n-1个结点,并且删除前编号为m-1,m-2的结点删除后编号变成了1,2。
如果假设删除之前的结点编号为old,删除了一个结点之后重新编号为new,由于本题设置的是编号从1开始,那么old与new之间的关系应为old = (new + m - 1)%n + 1(这里先+1再-1就是为了保证下标从1开始,如果new+m=n的话,会导致最后计算结果为old=0,然而对于这道题来说,由上表可知new=n-m是原来old=n的新编号,先+1再-1就是为了不让n变成0)。如果编号为从0开始,那么old与new之间的对应关系应为old = (new + m)%n


实现代码:

int f(int n, int m){
	return  n=1? n : ( f(n-1,m) + m - 1 ) % n + 1;
}

  对于递归的实现方式,其时间复杂度为O(n),空间复杂度为O(n)。


总结

  对于约瑟夫环的问题,其本身并不难理解,实现也不困难,对于循环链表和队列的常规实现方式思路清晰,容易理解。另外,递归的方式最重要的就是找到相邻两次遍历删除前与删除后的编号之间的关系,属于数学归纳一类,需要注意编号是从0开始还是从1开始的。
  对于这个问题,我只想到这几种方法,当然,还有最简单的数组,可以不断遍历数组,如果是第m个就将其存储元素设为-1,以此来解决这个问题,当然其时间复杂度还是O(m*n),如果有更快捷高效的方法,欢迎大家留言讨论。

约瑟夫环是一个经典的问题,它描述了一群人围成一个环,每次数到指定的数字就出圈,直到只剩下一个人为止。这个问题可以用队列来模拟。 以下是使用C语言实现约瑟夫环的队列形式的示例代码: ```c #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node* next; } Node, *Queue; // 初始化队列 void initQueue(Queue* Q) { *Q = (Queue) malloc(sizeof(Node)); (*Q)->next = *Q; } // 入队 void enQueue(Queue Q, int data) { Node* newNode = (Node*) malloc(sizeof(Node)); newNode->data = data; newNode->next = Q->next; Q->next = newNode; Q = newNode; } // 出队 int deQueue(Queue Q) { if (Q->next == Q) { // 队列为空 printf("Queue is empty!\n"); return -1; } Node* p = Q->next->next; int data = p->data; Q->next->next = p->next; if (p == Q) { Q = Q->next; } free(p); return data; } // 约瑟夫环 void josephus(int n, int m) { Queue Q; initQueue(&Q); for (int i = n; i > 0; i--) { enQueue(Q, i); } int count = 0; while (Q->next != Q) { int data = deQueue(Q); count++; if (count % m != 0) { enQueue(Q, data); } else { printf("%d ", data); } } printf("\n"); free(Q); } int main() { int n, m; printf("Please enter the number of people and the number to count: "); scanf("%d%d", &n, &m); josephus(n, m); return 0; } ``` 在上面的代码中,我们使用了一个循环链表来模拟队列,并实现队列的基本操作:初始化、入队、出队。在 `josephus` 函数中,我们先把所有人入队,然后进行循环,每次出队并计数,如果计数不到 `m`,就把这个人再次入队,否则就打印出这个人的编号。最后,当队列中只剩下一个人时,循环结束。 你可以尝试运行上面的代码,输入人数和每次要数的数字,看看约瑟夫环的结果是什么。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值