4,6
输出
2143
③输入(n,m均为正整数,且n=m)
7,7
输出
7136245
④输入(n,m中有浮点数)
8,5.56
输出
输入有误,请重新输入!
⑤输入(n,m中有负数)
-3,-8
输出
输入有误,请重新输入!
⑥输入(n,m未按格式输入)
aA2,3asf
输出
输入有误,请重新输入!
二、概要设计
抽象数据类型
n(n为正整数)个人的编号为1~n,1相当于唯一的“第一元素”,n相当于唯一的“最后元素”,除最后元素n外,剩下的n-1个编号均有唯一的后继,除第一元素1外,剩下的n-1个编号均有唯一的前驱,符合线性结构,故应以线性表实现该结构。
ADTalist
{
数据对象:
D={ai|ai∈int,i=1,2,…,n,n≥0}.
数据关系:
Rl={|ai-1,ai∈D,i=2,…,n}
基本操作:
InitList(&L,size)//构造一个空线性表L。
Append(&L,e)//新元素e入表
Remove(&L,i)//删除表中第i个元素,即将i之后的元素往前移一位。
DesList(&L)//销毁线性表,释放内存空间
}
无论选择哪种数据结构都应有一个结构初始化操作及相应的结构销毁操作,本程序的结构初始化操作为:
InitList(&L,size),结构销毁操作为DesList(&L)。
在将n个人的编号存进线性表时,需要一个添加操作,故设计Append(&L,e),其用于向线性表表尾添加新元素e。
“出列”相当于删除线性表中某个指定的元素,这就需要一个删除操作,故设计Remove(&L,i),其根据下标索引需要删除的元素,“删除”即相当于将该元素之后的所有元素向前移动一位。
算法的基本思想
n个人的编号为1,2,3,…,n(n>0),并且这n个人根据编号大小依序排列,即将这n个编号依序存入线性表中,报数值m为正整数。
①.编号为1的人从1开始依序报数,即从线性表的第一个元素起开始访问,并依序逐个访问它的下一个元素。
②.当轮到剩余人群中编号最大的人报数时,剩余人中编号最小的作为他的下一位,继续报数,即当线性表的表尾元素访问完后,将表头元素作为下一个访问对象。
③.数到m时报数停止,并且数到m的人出列,他的下一位从1开始重新报数。
即访问进行到第m次时停止,将第m次访问到的元素从表中删除,并在DOS界面上输出对应的编号,然后从它的下一个元素开始新一轮的访问(每轮m次)。
④.重复步骤2、3,直到出列n个人为止,即当表内元素均被删除为止,此时,整个出列序列已经输出在DOS界面上。
程序的流程
程序由三个模块组成:
(1)输入模块:
完成人数和报数值的输入,存入变量n,m中。
(2)功能模块:
设计一个实现约瑟夫问题的函数Joseph(&L,m),模拟循环报数,每出列一个元素,通过输出模块将其显示在屏幕上。
(3)输出模块:
屏幕上显示出列的元素。
三、详细设计
物理数据类型
输入的人数n与报数值m均应为正整数,为了能够存储和处理,变量n,m采用C语言中的int定义变量。
因为线性表是对n个人的编号1~n进行操作(编号1~n符合线性结构),过程中无需添加新的元素,即表内元素最多只有n个,为了减小空间上的开销,线性表采用顺序表来实现其物理结构。
线性表的每个元素存储的是n个人的编号1~n中的一个,所以线性表中元素类型定义为整型。
#defineDefaultListSize100//线性表默认长度为100
typedefintElem;
typedefintElemType;
typedefstructList
{
Elem*listArray;//存储空间基址
intlength;//当前长度
intmaxsize;//最大长度
}Alist;
voidInitList(Alist&L,intsize=DefaultListSize)//构造一个空线性表L
{
L.maxsize=size;//若未传参,size取默认值100
L.listArray=newElem[L.maxsize];
L.length=0;
}
voidDesList(Alist&L)//销毁线性表,释放内存空间
{
delete[]L.listArray;
}
boolAppend(Alist&L,ElemTypee)//新元素e入表
{
若L.length等于L.maxsize,返回false,表满,无法添加
L.listArray[L.length]=e;
L.length++;//由于有新的元素入表,故表的长度应该加1
返回true,表示新元素e入表成功
}
boolRemove(Alist&L,inti)//删除表中第i个元素,即将i之后的元素往前移一位。
{
若L.length等于0,返回false,表空,没有需要删除的元素
intj;
for(j=i;j{
L.listArray[j-1]=L.listArray[j];
}
L.length--;//由于从表中删除了一个元素,故表的长度应该减1
返回true,表示删除成功
}
算法的具体步骤
约瑟夫问题的实现(函数joseph(&L,m))算法流程图:
删除(基本操作Remove(&L,i))的算法流程图:
1.根据n的值新建一个长度为n的线性表,将编号1~n依次存入表中。
2.设变量t=0、i=L.length.
3.若i>=1(即线性表中至少还有一个未出列的元素),则t=(t+m-1)%i(即应出列元素的下标)。
4.输出下标为t的元素。
5.将出列元素之后的所有元素往前移一位,实现元素的“删除”;之后,i自减一次(从表中删除了一个元素)。
6.重复步骤3、4、5,直到表中无剩余元素。
算法的时空分析
算法的执行,主要是n个编号的出列,以及每次出列一个编号时,将其之后的编号向前移动一位(以达到“删除”目的),每次出列一个编号的时间代价为常量,记为c1,出列之后,将其之后的编号均前移一位的时间代价与n的大小有关,记为c2n,则整个算法执行的时间代价为:
(c1+c2n)*n=c1*n+c2n2,即:
Θ(n2)。
若采用循环链表实现线性表,则删除的时间代价仅为常数级,相应的算法代价只要Θ(n)。
函数的调用关系图
输入和输出的格式
输入
本程序用于解决约瑟夫环问题。
//提示
请输入人数n和报数值m,用逗号隔开,如“10,3”:
//提示
等待输入
输出
出列序列为:
//提示
//输出结果的位置,每个编号之间间隔两个空格长度
例如:
输入
本程序用于解决约瑟夫环问题。
请输入人数n和报数值m,用逗号隔开,如“10,3”:
12,6
输出
出列序列为:
612721085911143
4、调试分析
本题调试的难点主要是对输入合法性的验证。
输入的格式被设计为:
“n,m”。
要求先输入一个正整数n,再输入一个逗号,继续输入正整数m,最后按回车结束。
其他形式的输入均不合法。
scanf()函数的格式为:
scanf_s("%f,%f",&n,&m)。
scanf()函数按格式正确读入的返回值为2。
初步设计时,根据scanf()函数的返回值,以及成功读入后对n、m的验证(判断n、m是否为负数或浮点数)已能验证需求分析时设计的所有样例。
但是当输入格式为“1,2abc”、“4,21sf”的格式时(即整个输入的前部分是正确格式,如“1,2”、“4,21”),程序会有根据前部分正确格式计算得到的输出(如输入“1,2abc”时,输出“1”;输入“4,21”时,输出“1423”)。
此时的合法性验证中,只要输入时前部分的正确格式被scanf()读取,就会直接无视后半部分不合法的输入,这显然是不合理。
为了避免上述情况,我重新设计了输入合法性验证模块。
通过查阅资料了解到,scanf()成功按格式读入后,输入的回车‘\n’仍余留在输入缓冲区中。
根据这一特点,我首先通过getchar()函数来判断输入缓冲区的余留的第一个字符是否为回车‘\n’,若不是,则证明输入不合法,且该种不合法情况为有多余字符或字符串的输入,这就包括了上述形如“1,2abc”、“4,21sf”的情况,初始值为0的计数变量count在这种情况下加1,在循环的验证中根据count的值来排除这种情况;
若是,则通过检验scanf()的返回值和n、m是否为负数或浮点数来验证其合法性。
这样改进后就在原有案例能成功验证的基础上成功规避了上述不合理情况。
之后,又设计了多组测试数据,均能正确验证。
改进前的输入及合法性验证模块:
voidinput(ElemType&n1,ElemType&m1)//输入及合法性检查
{
floatn2,m2;
intjud;
do
{
jud=scanf_s("%f,%f",&n2,&m2);
if((jud!
=2)||(n2<=0)||(m2<=0)||(n2-int(n2))!
=0||(m2-int(m2))!
=0)
{
printf("输入有误,请重新输入!
\n");
while(getchar()!
='\n'){};/*清空输入缓冲,可能有字符串输入,所以用了循环*/
}
}while((jud!
=2)||(n2<=0)||(m2<=0)||(n2-int(n2))!
=0||(m2-int(m2))!
=0);
n1=int(n2);
m1=int(m2);
}
改进后的输入及合法性验证模块:
voidinput(ElemType&n1,ElemType&m1)//输入及合法性检查
{
floatn2,m2;
intjud,count=0;
do
{
count=0;
jud=scanf_s("%f,%f",&n2,&m2);//按格式成功读入返回2
if(getchar()=='\n')//scanf成功读取后仍会余留回车符
{
if((jud!
=2)||(n2<=0)||(m2<=0)||(n2-int(n2))!
=0||(m2-int(m2))!
=0)
{
printf("输入有误,请重新输入!
\n");
continue;
}
}
else
{
printf("输入有误,请重新输入!
\n");
count++;
while(getchar()!
='\n'){};//清空输入缓冲,可能有字符串输入,所以用了循环
}
}while((jud!
=2)||(n2<=0)||(m2<=0)||(n2-int(n2))!
=0||(m2-int(m2))!
=0||count!
=0);
n1=int(n2);
m1=int(m2);
}
改进前:
输入(前部分为正确格式)
3,5sd
输出
2,3,1
截图:
改进后:
输入(前部分为正确格式)
3,5sd
输出
输入有误,请重新输入!
截图:
5、测试结果
①输入(n,m均为正整数,且n>m)
10,3
输出
36927185104
测试截图:
②输入(n,m均为正整数,且n4,6
输出
2143
测试截图:
③输入(n,m均为正整数,且n=m)
7,7
输出
7136245
测试截图:
④输入(n,m中有浮点数)
8,5.56
输出
输入有误,请重新输入!
测试截图:
⑤输入(n,m中有负数)
-3,-8
输出
输入有误,请重新输入!
测试截图:
⑥输入(n,m未按格式输入)
aA2,3asf
输出
输入有误,请重新输入!
测试截图:
6、用户使用说明
1、本程序的运行环境为Win764位操作系统,执行文件为Exp1josephus.exe
2、运行程序时
提示输入人数和报数值,注意人数和报数值均应为正整数,输入不合法时要求重新输入,在输入时,两者用逗号隔开。
本程序用于解决约瑟夫环问题。
请输入人数n和报数值m,用逗号隔开,如“10,3”:
输出
出列序列为:
7、实验心得
①.需求分析中的测试案例设计要尽量考虑全面,案例的输入形式应能基本覆盖所有可能的情况,对各种极端输入应都能进行正确的合法性验证。
在设计时切记谨慎。
针对我在调试时遇到的情况可知,合法性验证绝非易事,特别是在有一定输入格式的时候。
在scanf()从输入的部分中读取到正确格式时,还要考虑是否有未被读到的部分,若有,即使scanf()获取到正确格式的输入,该种情况也是不合法的,要及时规避。
若感觉输入合法性验证的设计繁琐复杂,可先查阅资料,了解相应的输入输出函数的具体运作,不保证能解决困难,但一定会有所收获。
②.在设计每个问题的求解算法必须基于抽象数据类型的基本操作来实现,直接基于物理数据结构来实现是不可取的。
③.设计算法的基本思想时,应遵照题目具体要求,明白最后到底要得到什么或输出什么,再开始设计算法,并通过算法实现或者得到最后结果,只有经过这样的流程设计出的算法才是正确的。
八、附录
注:
由于本程序是用C写的,为避免误会,所有基本操作函数名首字母大写,并有注释说明。
#include
#include
#defineDefaultListSize100
typedefintElem;
typedefintElemType;
typedefstructList
{
Elem*listArray;//线性表的基地址
intlength;//线性表的当前长度
intmaxsize;//线性表的最大长度
}Alist;
voidInitList(Alist&L,intsize=DefaultListSize)//结构初始化操作,构造一个空线性表L
{
L.maxsize=size;
L.listArray=newElem[L.maxsize];
L.length=0;
}
voidDesList(Alist&L)//结构销毁化操作,销毁线性表,释放内存空间
{
delete[]L.listArray;
}
boolAppend(Alist&L,ElemTypee)//添加操作,新元素e入表
{
if(L.length==L.maxsize)
returnfalse;
L.listArray[L.length]=e;
L.length++;
returntrue;
}
boolRemove(Alist&L,inti)/*删除操作,删除表中第i个元素,即将i之后的元素往前移一位。
*/
{
if(L.length==0)
returnfalse;
intj;
for(j=i;j{
L.listArray[j-1]=L.listArray[j];
}
L.length--;
returntrue;
}
voidcreatlist(Alist&L,ElemTypen)//将元素存入线性表
{
for(inti=1;i<=n;i++)
{
Append(L,i);
}
}
voidinput(ElemType&n1,ElemType&m1)//输入及合法性验证
{
floatn2,m2;
intjud,count=0;
do
{
count=0;
jud=scanf_s("%f,%f",&n2,&m2);//按格式成功读入返回2
if(getchar()=='\n')//scanf成功读取后仍会余留回车符
{
if((jud!
=2)||(n2<=0)||(m2<=0)||(n2-int(n2))!
=0||(m2-int(m2))!
=0)
{
printf("输入有误,请重新输入!
\n");
continue;
}
}
else
{
printf("输入有误,请重新输入!
\n");
count++;
while(getchar()!
='\n'){};/*清空输入缓冲,可能有字符串输入,所以用了循环*/
}
}while((jud!
=2)||(n2<=0)||(m2<=0)||(n2-int(n2))!
=0||(m2-int(m2))!
=0||count!
=0);
n1=int(n2);
m1=int(m2);
}
voiddisplay(Alist&L,inta)
{
printf("%d",L.listArray[a]);
}
voidjoseph(Alist&L,ElemTypem)//解决约瑟夫环问题的核心代码
{
inti,t;
t=0;
for(i=L.length;i>=1;i--)
{
t=(t+m-1)%i;
display(L,t);
Remove(L,t+1);
}
}
voidmain()
{
intn,m;
AlistL;
printf("本程序用于解决约瑟夫环问题。
\n");
printf("请输入人数n和报数值m,用逗号隔开,如“10,3”:
\n");
input(n,m);//输入n、m并进行合法性验证
InitList(L,n);//构造一个空线性表L
creatlist(L,n);//将元素存入线性表
printf("出列序列为:
\n");
joseph(L,m);//约瑟夫环问题的实现
DesList(L);//释放内存空间
system("Pause>>NULL");
}