约瑟夫环问题(1)王红梅老师的解法

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博客中有同样的用法??

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值