C语言如何解决约瑟夫环问题
作者:happy life 2022
文章总结了四种解决特定问题的方法,包括单循环链表法、循环数组法、递归法和迭代法,并分享了个人经验
题目
解答
法一:单循环链表
#include<stdio.h> #include<stdlib.h> //定义单循环链表数据结构 typedef struct CNode{ int data; struct CNode *next; }CNode; typedef CNode *CLinkList; //初始化一个指向头节点的指针,使头指针->next=头指针,头指针->data不做处理 void InitList_L(CLinkList *L){ (*L) = (CLinkList)malloc(sizeof(CNode)); if(!(*L)) exit(-2); (*L)->next = *L; } //头插法建立单循环链表 int CreateList_HL(CLinkList *L,int s[],int n){ //二级指针 int i; CLinkList p; *L = (CLinkList)malloc(sizeof(CNode)); if(!(*L)) exit(-2); (*L)->next = *L; //只有一个头节点head,就让next指向自己 for(i=0; i<n; i++){ p = (CLinkList)malloc(sizeof(CNode)); if(!p) exit(-2); p->data = s[i]; //录入数据 p->next = (*L)->next; //将头指针所指向的下一个结点的地址,赋给新创建结点的next (*L)->next = p; //将新创建的结点的地址赋给头指针的下一个结点 } return 1; } //尾插法建立单循环链表 int CreateList_TL(CLinkList *L,int s[],int n){ int i; CLinkList p, q; *L = (CLinkList)malloc(sizeof(CNode)); if(!(*L)) exit(-2); (*L)->next = *L; //只有一个头节点head,就让next指向自己 for(i=0,q=*L; i<n; i++){ p = (CLinkList)malloc(sizeof(CNode)); if(!p) exit(-2); p->data = s[i]; //录入数据 q->next = p; q = q->next; } q->next = *L; //最后一个结点指向head return 1; } //求单循环链表的长度 int ListLength(CLinkList L){ CLinkList p; int count; if(L){ count = 0; p = L; //p指向头结点 while(p->next!=L){ //p没到表头 count++; p = p->next; } } return count; } //得到指向单循环链表第i个元素的指针 CLinkList GetElemPtr(CLinkList L, int i){ int count; CLinkList p; if(L&&i>0){ count = 1; p = L->next; while(p!=L && count<i){ count++; p = p->next; } if(p!=L) //L为头指针 return p; } return NULL; } //单循环链表第i个位置插入元素e int ListInsert(CLinkList L, int i, int e){ CLinkList p, s; int j; if(i<1 || i>ListLength(L)+1) //插入位置不合理 return 0; p = L; j = 0; while(j<i-1){ //寻找第i-1个结点 p = p->next; ++j; } s = (CLinkList)malloc(sizeof(CNode)); if(!s) exit(-2); s->data = e; s->next = p->next; p->next = s; return 1; } //单循环链表第i个位置删除元素e int ListDelete(CLinkList L, int i, int *e){ CLinkList pre, p; int j; if(i<1 || i>ListLength(L)) //删除位置不合理 return 0; pre = L; j = 1; while(pre->next && j<i){ //寻找第i个结点,并令pre指向其前驱 pre = pre->next; ++j; } p = pre->next; pre->next = p->next; *e = p->data; free(p); p=NULL; return 1; } //遍历单循环链表 void ListTraverse(CLinkList L){ CLinkList p; p = L->next; //p指向头结点,正向访问链表 while(p!=L){ printf("%d ",p->data); p = p->next; } printf("\n"); } //判断单循环链表是否为空 int ListEmpty(CLinkList L){ if(L!=NULL && L->next==L) //单循环链表存在并且只有头结点 return 1; else return 0; } /* 用单循环链表解决约瑟夫环问题(带头结点) 这里的单循环链表使用了带头结点的,方便找到表头的位置,但是由于头结点不存储元素,因此需要跳过头结点 链表插入删除都比较方便,不需要移动大量元素,只需要移动指针即可,适合这一类问题 */ void Algo(CLinkList L,int k,int m){ //从编号为k的人开始数,数到m的那个人出队列 int count,i,j; //count表示每次从1开始数,i用来找编号为k的结点的前驱 CLinkList pre,p; pre=L; count=1,i=1,j=1; /*寻找第k个结点,并令pre指向其前驱*/ while(i<k){ if(pre->next==L) //跳过头结点 pre=pre->next->next; else pre = pre->next; ++i; } while(L->next!=L){ //如果单循环链表不为空 if(count==m){ /*下一个结点不是头结点,直接删除*/ if(pre->next!=L){ p=pre->next; printf("第%d次出环的元素是%d\n",j++,p->data); pre->next=p->next; free(p); p=NULL; count=1; } /*下一个结点是头结点,下下个结点不是头结点,跳过头结点,删除下下个结点(头结点不保存数据,因此不参与运算)*/ else if(pre->next->next!=L){ p=pre->next->next; printf("第%d次出环的元素是%d\n",j++,p->data); pre->next->next=p->next; free(p); p=NULL; count=1; } /*下一个结点是头结点,下下个结点也是头结点,说明单循环链表已经为空了,只剩下头结点,因此跳出循环*/ else break; } count++; //count代表要删除的结点数的数,始终在pre指向的结点之前 if(pre->next==L) //跳过头结点 pre=pre->next->next; //pre指向要删除结点的前一个结点 else pre = pre->next; //pre指向要删除结点的前一个结点 } } void main(){ CLinkList L; int n=5,s[5]={1,2,3,4,5}; //假设5个人围坐一圈 int k=3,m=2; //第一次从编号为k的人开始数,数到m的那个人出队列 CreateList_TL(&L,s,n); //头插法建立单循环链表 ListTraverse(L); //遍历原始队列 printf("假设第一次从编号为%d的人开始数,数到%d的那个人出环\n",k,m); Algo(L,k,m); //用单循环链表解决约瑟夫环问题(带头结点) }
法二:循环数组
/* 用循环数组解决约瑟夫环问题 解决问题的难点有两个: 1、如何求下一个的人的位置:在循环数组中向后移动采用:(k+l)%n 2、此人出圈后对这个人的位置如何处理:先将出圈人的位置打印输出,然后将其位置元素设置为0,表示它已出圈,以后见到它就直接跳过去 */ void Algo2(int s[],int n,int k0,int m){ //开始一共有n个人,从第k0个人开始数,数到m的那个人出队列 int i,j,k=k0-1; //元素访问的下标为 k,初始时从第k0个人开始数 for(i=0;i<n;i++){ //总共循环n次,每循环一次,出队一个元素,k在循环中会不断的运动 j=1; //j表示数的数,初始为1 while(j<m){ //如果还没有数到m while(s[k]==0) //如果s[k]为0,证明它已经出圈了,则跳过它 k=(k+1)%n; j++; //数下一个数 k=(k+1)%n; //数组下标向后走一步 } while(s[k]==0) //如果数到m发现它出圈了,则跳过它,找到第一个没出圈的数 k=(k+1)%n; printf("第%d次出环的元素是%d\n",i+1,s[k]); //先将出圈人的位置打印输出 s[k]=0; //然后将其位置元素设置为0,表示它已经出圈 } } void main(){ int n=5,s[5]={1,2,3,4,5}; //假设5个人围坐一圈 int k=3,m=2; //第一次从编号为k的人开始数,数到m的那个人出队列 printf("假设第一次从编号为%d的人开始数,数到%d的那个人出环\n",k,m); Algo2(s,n,k,m); //用循环数组解决约瑟夫环问题 }
法三:递归
/* 用递归解决约瑟夫环问题 n指的是总人数,m指的是每次最大报到的数值,i是第i次,该函数每次可以求出第i次扔海里的人的编号 */ int Algo3(int n,int m,int i){ if(i==1) return (n+m-1)%n; else return (Algo3(n-1,m,i-1)+m)%n; } void main(){ int n=5; //假设5个人围坐一圈 int m=2; //数到2的那个人出环 printf("假设第一次从编号为1的人开始数,数到%d的那个人出环\n",m); for(int i=1;i<=n;i++) printf("第%d次出环的元素是%d\n",i,Algo3(n,m,i)+1); //因为求出的结果是数组中的下标,最终的编号还要加1 }
法四:迭代
/* 用迭代解决约瑟夫环问题 递推公式: f(N,M)=(f(N−1,M)+M)%N f(N,M)表示,N个人报数,每报到M时杀掉那个人,最终胜利者的编号 f(N−1,M)表示,N-1个人报数,每报到M时杀掉那个人,最终胜利者的编号 */ int Algo4(int n,int m){ int i,p=0; for(i=2;i<=n;i++){ p=(p+m)%i; } return p+1; //因为求出的结果是数组中的下标,最终的编号还要加1 } void main(){ int n=5; //假设5个人围坐一圈 int m=2; //数到2的那个人出环 printf("假设第一次从编号为1的人开始数,最后出环的是:%d\n",Algo4(n,m)); }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。