1问题描述
设有编号为1,2,3,....n的n(n>0)个人围成一个圈,每个人持有一个密码m,从第一个人开始报数,报到m时停止报数,报m的人出圈,再从他的下一个人起重新报数,报到m时停止报数,报m的人出圈,........如此下去,直到所有人全部出圈为止。当给定n和m后,设计算法求n个人出圈的次序:
这是一个比较综合的问题,其求解步骤如下:
(1)分析问题,对问题建立数据模型
(2)根据问题的特点和运算为数据模型设计适当的存储结构
(3)基于存储结构设计相应的算法
(4)调试 算法,上机实现
2分析问题
分析问题最好的方法就是运行一个实例。
首先考虑比较简单的情况:每个人持有密码相同,假设n=6,m=3时约瑟夫环问题的求解过程:
注意:出圈顺序和报数规律
通过上述问题求解过程,可把问题的输入(即n个人的编号)看成是一个线性序列,每个人的编号看成是一个数据元素。因此,问题抽象出的数据模型是线性表(逻辑结构)
引申:针对一个待求解问题,只有抽象出数据模型,才能为其设计合适的数据结构,从而实现数据从机外表示到机内表示的转化
引申:一种存储结构设计是否合理,取决于运算是否方便和时间性能是否高效。实际中,可设计几种方案,经过分析比较选择最合理的存储结构。
3设计存储结构
线性表有2种基本存储结构:顺序存储结构和链式存储结构
(1)顺序存储结构
用顺序表存储约瑟夫环问题的输入数据(即n个人的编号),需要考虑以下问题:ab
a如何表示某人已出圈使之不参与下一轮报数,可以有2种解决方法
方案一:将出圈的人从顺序表中删除。由于顺序表中执行删除运算需要移动元素,降低问题求解效率
b当报数到表尾时如何再回到表头使计数循环进行下去
可以使用求模运算实现循环计数。
采用方案一,则顺序表的长度是变化的,需要在每次出圈后重新设定表长;
采用方案二,则顺序表长度不变,但是在计数时要先判断是否为出圈标志
(2)链式存储结构
由于约瑟夫环问题本身具有循环性质,考虑采用循环链表;为了统一对表中任意节点的操作,循环链表不带头结点。
问题1:采用循环链表,是否需要头结点?
问题2:如何定义该循环链表的结构?
在链表上实现删除操作无须移动元素,只需修改指针(所有链表的优点)
4设计算法
(1)顺序存储结构实现
方案一:设length为当前表长,则每出圈一人表长减1,可用出圈次数控制整个循环。设count用来计数,当count数到m,将对应的人出圈
int main()
{
int a[100]={0};
int n=6;//n代表总人数,m代表报到几退出
int m=3;
int s=0;//数组下标
for(int j=0;j<n;j++)a[j]=j+1;
for(int length=n;length>1;length--)//每次出圈表长减1,有n-1个人出圈
{
for(int count=0;count<m-1;count++)
{
s=(s+count)%length;//count==m-1退出循环
}
s=(s+1)%length;//由于上面的count==m-1退出循环,s+count没有统计到m个数,所以s=(s+1)%length
cout<<a[s]<<endl;
for(int i=s+1;i<length;i++)//向前移动,物理覆盖法删除
a[i-1]=a[i];
}
cout<<"-----"<<endl;
/* for(int i=0;i<n;i++){
cout<<a[i]<<endl;
}
*/
cout<<a[0]<<endl;//根据方案一,其他均被删除,剩下第0号元素,最后只剩一个人
return 0;
}
上面根据王红梅老师教师用书进行编写(王红梅老师在这一点上写的有一点瑕疵)
下面的问题很重要:
a如何在计数和存储下标之间建立对应关系?
利用s作为数组下标,通过s和count的相加取余,见下面问题的for循环
b如何实现循环计数?
for(int count=0;count<m-1;count++)
{
s=(s+count)%length;//count==m-1退出循环
}
c最后剩下的一个人存储在数组什么位置?0号位置
下面为一个错误代码,为什么?
int main()
{
int a[100]={0};
int n=6;//n代表总人数,m代表报到几退出
int m=3;
int s=0;//数组下标
for(int j=0;j<n;j++)a[j]=j+1;
for(int length=n;length>1;length--)//每次出圈表长减1,有n-1个人出圈
{
for(int count=1;count<m;count++)//修改了,这里,外层for循环进入,外层循环前s=0,在内层循环直接加1,没有统计0号单元
{
s=(s+count)%length;
}
cout<<a[s]<<endl;
for(int i=s+1;i<length;i++)//向前移动,物理覆盖法删除
a[i-1]=a[i];
}
cout<<"-----"<<endl;
/* for(int i=0;i<n;i++){
cout<<a[i]<<endl;
}
*/
cout<<a[0]<<endl;//根据方案一,其他均被删除,剩下第0号元素
return 0;
}
for(int count=1;count<m;count++)//修改了,这里,外层for循环进入,外层循环前s=0,在内层循环直接加1,没有统计0号单元
{
s=(s+count)%length;
}
因为如下代码:
for(int i=s+1;i<length;i++)//向前移动,物理覆盖法删除
a[i-1]=a[i];
因为覆盖了,下次查找 仍从下标s开始,所以count只能初始化为0(可以自己跟踪调试)
方案二:
int main(int argc, char* argv[])
{
int a[100]={0};//初始化数组作为环
int count=0;//记录退出的个数
int k=-1;//从这里假设开始为第1个人,下标为0编号为1
int n=6;//n代表总人数,m代表报到几退出
int m=3;
for(int j=0;j<n;j++)a[j]=j+1;//对环进行赋值
while(count<n-1){
int i=0;//i记录当前报数编号
while(i<m){
k=(k+1)%n;//k为循环下标处理
if(a[k]!=0)
{
i++;
if(i==m){a[k]=0;count++;}//报到m,设置为0表示出圈
}
}
}
for(int i=0;i<n;i++){
if(a[i]!=0)
{
cout<<a[i]<<endl;break;
}
}
return 0;
}
补充:出圈标志位为?(-1)
这种方法简单吗?思路简单,但是编码却没那么简单,临界条件特别多,每次遍历到数组最后一个元素的时候,还得重新设置下标为 0,并且遍历的时候还得判断该元素时候是否是 -1。用这种数组的方式做,千万不要觉得很简单,编码这个过程还是挺考验人的。
这种做法的时间复杂度是 O(n * m), 空间复杂度是 O(n);
下面给出数组方法的参考代码:
#include<algorithm>
#include<iostream>
using namespace std;
int main(){
int a[1001]={0}; //初始化化数组作为环
int n,m;//n代表总的人数,m代表报数到几退出
cin>>n>>m;
int count=0;//记录退出的个数
int k=-1;//这里假定开始为第一个人,下标为0,编号为1,如需从编号x开始,则k=x-2
while(count<n-1){ //总共需要退出n-1个人
int i=0;//记录当前报数编号
while(i<m){
k=(k+1)%n; //循环处理下标
if(a[k]==0){
i++;
if(i==m){
a[k]=-1;
count++;
}
}
}
}
for(int i=0;i<n;i++){
if(a[i]==0){
printf("%d\n",i+1);
break;
}
}
return 0;
}
对于:代码中k=-1,在前面的kmp博客中有同样的用法??
(2)链式存储结构的实现
设工作指针p指向当前计数节点,为实现删除节点p的操作,再设辅助工作指针pre指向节点p的前驱节点,为便于工作指针的初始化,将指针pre初始化为开始节点,将指针p初始化为开始节点的后继,计数从2开始。
rear->next=first;//这是循环单链表区别单链表的地方(重点)
写代码,多举几个实例,不要着急!!!!
// 约瑟夫环头插法建立链表实现.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include<stdio.h>
#include<malloc.h>
typedef struct Node{
int data;
struct Node* next;
}Node;
Node* create(int n)//头插法
{
Node* first=NULL,*s;
int i;
first=(Node*)malloc(sizeof(Node));
first->data=n;
first->next=first;//循环链表
Node* rear=first;//因为是头插法,第一个插入的节点,在完全插入后成为最后一个节点
for(i=n-1;i>=1;i--)//里面的插入操作和单链表无头结点的操作一样
{
s=(Node*)malloc(sizeof(Node));
s->data=i;
s->next=first;//头插
first=s;//s成为第一个节点
}
rear->next=first;//这是循环单链表区别单链表的地方(重点)
return first;
}
void traverse(Node* first)//test,遍历,里面循环条件是(-重点-)
{
Node* p=first;
while(p->next!=first)//这个地方p->next!=p死循环(错了:为什么死循环因为是循环链表);first是正确的
{
printf("%-3d",p->data);
p=p->next;
}
printf("%-3d",p->data);//因为此时p->next==s,所以p是最后一个节点
printf("\n");
}
void josph(Node *first,int m)
{
Node* pre=first;
Node* p=first->next;
int count=2;
while(pre!=p)
{
if(count<m)
{
pre=p;p=p->next;
count++;
}
else{
printf("%-3d",p->data);
pre->next=p->next;free(p);
p=pre->next;
count=1;
}
}
printf("%-3d",p->data);free(p);
}
int main(int argc, char* argv[])
{
Node* first=NULL;
int n=6;int m=3;
first=create(n);
// traverse(first);
josph(first,m);
printf("\n");
return 0;
}
代码如下:
#include<stdio.h>
#include<malloc.h>
typedef struct Node{
int data;
struct Node* next;
}Node;
Node* create(int n)//利用的是尾插法
{
Node *rear=NULL,*s;
int i;
rear=(Node*)malloc(sizeof(Node));//建立长度为1的循环单链表
rear->data=1;
rear->next=rear;//循环链表标志,为rear->next初始化(容易出错,小心,小心!!!)
for(i=2;i<=n;i++)
{
s=(Node*)malloc(sizeof(Node));
s->data=i;
s->next=rear->next;//将节点s插入尾节点rear后面
rear->next=s;
rear=s;//rear指向当前的尾节点
}
return rear;//结束函数,返回尾指针rear
}
void joseph(Node* rear,int m)//rear为循环链表的尾指针,形参m是密码
{
Node* pre=rear,*p=rear->next;//pre为尾指针,p指向第一个节点
int count=1;
printf("出环顺序:\n");
while(p->next!=p){//循环直到循环链表只剩一个节点
if(count<m)//计数器已经累加到密码值
{
pre=p;p=p->next;//工作指针分别后移,注意这两句话的顺序
count++;//
}
else//计数器已经累加到m
{
printf("%-3d",p->data);//打印出环编号
pre->next=p->next;//节点p删除
free(p);//
p=pre->next;//工作指针p后移,但pre不动
count=1;//计数器从1开始重新计数
}
}
printf("%-3d",p->data);//输出最后一个节点
free(p);//
}
int main(int argc, char* argv[])
{
int n=6;
int m=3;
Node *rear=NULL;
rear=create(n);joseph(rear,m);
printf("Hello World!\n");
return 0;
}
总结:
循环单链表:
(1)循化单链表头插法和尾插法(注意指针修改的细节)
(2)循环单链表,进行查询的条件:p=first;while(p->next!=first){}
p->next==first:说明p是最后一个节点
(3)在单循环链表进行删除时,判断条件:while(p->next!=p)在joseph函数中
(4)在链表中,pre=p,p=p->next(pre为跟踪指针,p为工作指针)
顺序存储:
(1)顺序存储表示删除的2种方法:方案一移动元素物理覆盖法(长度改变)和方案二存储特殊标志(长度不变)
(2)求模运算实现循环
(3)对于:代码中k=-1,在前面的kmp博客中有同样的用法??