数据结构与算法——约瑟夫环

这篇具有很好参考价值的文章主要介绍了数据结构与算法——约瑟夫环。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一、例题引入

        # 解题思路

        #图例分析

        #代码段

        #题解小结

 二、循环链表

        分析:

        直接看代码:

 三、标记数组

        分析:

        代码:

四、递归算法 

        #沿用解释


一、例题引入

        设有n个人坐在圆桌周围,从第s个人开始报数,数到m时的人出列,接下来出列后的下一个人开始报数,同样是数到m的人出列,依次重复,直至所以人都出列,输出其出列的顺序。

        # 解题思路

        题解有很多种,我们这先用单链表来分析:

题目分析:本题可以先根据圆桌周围的n个人构造一个单链表,设置三个指针p、q、s,使q指向找到的第s个人对应的前驱结点,p指向第s个人对应的结点,由此进入两重for循环:第一重循环为n-1次(即要出列(输出)n-1个人,剩余一个后面再直接输出),第二重循环为寻找报数为m的人对应的结点。开始报数,使p、q依次后移(p=p->next,q=q->next)来找第m个人报数的对应的结点位置。但这会遇到一个问题:单链表的长度为n,若当s+m>n时继续查找会发生链表非法访问(NULL),所以我们必须先判断p、q的下一结点是否为空来进行处理:如果p、q两指针的下一结点都不空,即p第一遍后移还没到表尾,则p=p->next;q=q->next;若p的下一结点为NULL,即p为表尾,则让p=h->next(头结点的下一个,也是第一个结点),q=q->next;若q的下一结点为空,即指针p之前已经后移至表尾且现在已经指向头结点的下一结点,则令q=h->next;p=p->next; 此时就已经找到了报数位m的人对应的结点以及他对应的前驱结点,让r指向p(准备释放结点p),输出p->info,让p=p->next,这时能够输出第一个出列的人,但为了后面继续查找(报数)并输出下一个出列的人,还得判断p、q两指针的位置并修改它们的指向:若p->next=NULL(即p为表尾),则head->next=p;q->next=NULL;若p->next!=NULL&&q->next!=NULL(即p、q的下一结点都不为空),则q->next=p(即让q为p的前驱);若p->next!=NULL&&q->next==NULL(即p此时已经为第二个结点,而q为表尾),则让头结点的下一个结点指向p,即head->next=p;此时p、q都找到了能够开始下一次循环的位置,释放r结点。依次重复上述操作使对应的人出列,直至链表为只剩一个结点,即为头结点的下一个结点,输出head->next->info ,结束

        #图例分析

        单看思路分析可能会有点乱,让我们先根据下列图例进一步理解,我们让人数n为6,报数开始位置s为3,数数到m为2

        1.找到报数开始的位置

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

        2.开始报数直至m,此时找到了要出列的人对应的结点以及他的前驱结点,让p出列(输出p->info),让r指向p,准备释放,p、q仍然不为表尾,所以p=p->next;q->next=p;

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

        3.释放r,执行下一次出列操作。又从1开始报数,这时p为表尾,就让其指向第一个结点,即p=head->next,同时让q=q->next(q现在成了表尾),再继续报数,因为q现在为表尾,则让q=head->next;p=p->next;再一次数数到了2,令r=p;准备释放,p=p->next;q->next=p;

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

        4. 重复上述过程,直到n-1人出列,表中剩余最后一个结点,在循环外出列,即printf("%d",head->next->info),结束

        #代码段

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef char datatype;

typedef struct _node
{
	datatype info;
	struct _node *next;
}_node;

_node* CreateLink(int n);//建立有n个结点并带头结点的链表
void joseph(int n, int s, int m);



_node* CreateLink(int n)  //建立有n个结点并带头结点的链表
{
	_node *head, *p, *q;
	if (n == 0)
	{
		return NULL;
	}

	head = (_node*)malloc(sizeof(_node));  //申请一个结点为表头
	assert(head != NULL);  //断言函数,不为空
	q = head;  //q指向头结点
	for (int i = 0; i < n; i++)
	{
		p = (_node*)malloc(sizeof(_node));
		assert(p != NULL);
		p->next = NULL;
		printf("please input element :");
		getchar();
		scanf("%c", &p->info);
		q->next = p;
		q = p;    //q指向表尾,继续循环准备插入下一个结点
	}
	return head;  //返回头结点
}




void joseph(int n, int s, int m)//有n个人,从第s个人开始数数,报道m的人出列
{
	_node *CreateLink(int); //函数说明
	_node *p, *q, *r, *h;

	if (s > n)
	{
		return;  //找不到该结点
	}

	h = CreateLink(n);//函数调用,建立一个含有n个结点的链表
	q = h;
	for (int i = 0; i < s - 1; i++)
	{
		q = q->next;  //找到s的前驱结点
	}
	p = q->next;    //p指向s结点

	for (int i = 1; i < n; i++)   //循环n-1次,剩一个留在后面直接输出
	{
		for (int j = 1; j < m; j++)   //从当前位置开始出发找第m个结点(报数)
		{
			if (p->next != NULL&&q->next != NULL) 
			{//都为非空,即指向的下一个结点不是表尾,指针下移
				p = p->next;
				q = q->next;
			}
			else     //至少有一个是表尾
			{
				if (p->next == NULL)
				{
					p = h->next;
					q = q->next;
				}
				else  //q所指的下一个结点为表尾
				{
					q = h->next;
					p = p->next;
				}
			}
		}
		printf("%c ", p->info);//一个元素出列
		r = p;  //让r指针指向p准备释放
		if (p->next == NULL)  //p为链表尾
		{
			p = h->next;  //p指向第一个结点(即头结点的下一个)
			q->next = NULL;
		}
		else
		{
			p = p->next;//p后移一位
			if (q->next != NULL)
			{
				q->next = p;
			}
			else
			{//q为链表尾
				h->next = p;//跳过原来的第一个结点,后面释放
			}
		}
		free(r);
	}
	printf("%c", h->next->info); //输出剩余的一个人
}

int main()
{
	int n, s, m;//n个人,从s开始的第m个位置
	printf("please input n,s m:");
	scanf("%d %d %d", &n, &s, &m);
	joseph(n, s, m);

	system("pause");
	return 0;
}

        #题解小结

        基于上面的论述,我们能够用数据结构中处理单链表的方法来解决约瑟夫环的问题,通过更改p、q两指针的指向一步步的查找并出列数到m的人,但过程很繁琐,解决问题的方式不佳,所以下面我们将介绍另外几种算法,进一步优化并加深大家对该类问题的理解。

 二、循环链表

        分析:

        既然前面都能用单链表解决问题了,那对于更灵活的循环链表而言,不需要考虑单链表所谓非法访问的问题,也不需要频繁的改变指针后移的语句,只需要及时的删除出列的人对应的结点就行,那么岂不是简简单单!

        直接看代码:

#include<iostream>

typedef struct _node  //重命名
{
	int data;
	struct _node* next;
}_node;

//初始化循环链表
_node* Init_List(_node *head)
{
	_node *p;
	head->data = 1;
	head->next = NULL;
	p = head;

	return p;
}

_node* push_back(_node *p, int n)//尾插
{
	for (int i = 2; i <= n; i++)
	{
		_node *r = (_node*)malloc(sizeof(_node));
		r->data = i;
		r->next = NULL;
		p->next = r;
		p = r;
	}
	return p;//返回表尾

}

int main()
{
	int n, m;//n个人,报数到m出列
	std::cin >> n >> m;

	_node *head, *p, *q, *r;
	head = q = r = (_node*)malloc(sizeof(_node));//开辟空间
	assert(head != NULL);

	p = Init_List(head);//初始化
	p= push_back(p, n);//尾插创建链表,同时构造循环链表

	p->next = head;//创建循环链表,使表尾指向表头
	p = p->next;

	while (p->next != p)//判断是否只有一个元素
	{
		for (int i = 1; i < m; i++)
		{
			q = p;//出列人的前驱结点
			p = p->next;
			r = p;//准备释放
		}
		std::cout << p->data<<" ";//出列
		q->next = p->next;
		p = p->next;//更新位置准备下一次循环
		free(r);
	}
	std::cout << p->data;
	//system("pause");
	return 0;
}

 三、标记数组

        分析:

        一开始设置一个标记数组mark[100],让其初始化为0,0表示未出列,如若有人出列,则将其对应的位置mark[i]赋值为1,代表该人已经出列,不在需要报数,同时定义一个计数器count,判断所有人是否都已经出列,是则结束循环。

        代码:

#include<iostream>

//使用mark标记数组来实现约瑟夫环问题
int main()
{
	int mark[100] = { 0 };//初始化为0代表未出列
	int m, n, count=0, i=0, j=0;  //count用来计出列人数,判断是否大于>n
	std::cin >> n >> m;//一共n个人,数到m出列(这里规定从第一人开始)

	while (count <= n)
	{
		i++;  //每个人的编号
		if (i > n)
		{
			i = 1;  //当i超出n时让其从头开始报数,即i=1
		}
		if (mark[i] == 0)//当前编号未出列,继续报数
		{
			j++;
			if (j == m)//数到m应该出列
			{
				mark[i] = 1;//出列
				std::cout << i << " ";
				j = 0;//重新报数
			}
		}
	}
	return 0;
}

四、递归算法 

        #沿用解释

        这一段比较难理解,本人水平有限,不能很好地将该递归方式的过程解释出来,所以下面我截取了他人的一部分理解,详细内容请参见博客约瑟夫环问题_小C哈哈哈的博客-CSDN博客

⭕递归方式(前提要求:学习过递归并且已经掌握递归的基本使用)— 这种方式可以不看,因为它确实较难理解😔不过还是要有信心学习😄
既然是利用递归求解,那么我们首先肯定要明确,递归函数所代表的含义,这里我暂且将递归函数命名为ysf:ysf(int N,int M,int i):这个递归函数代表的意思为:有N个人,报到数字M时出局,求第i个人出局的编号。

比如:ysf(10,3,1)=2(假设人从0开始编号) 代表的意思就是有10个人,报到数字3时出局,第1个出局的编号为2

我们先来推:当i为1时,出局的人编号的数学公式,我们来看一个例子:假设现在总共有10个人,从0开始编号,那么就是0-9,如下图所示

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

现在,M=3,i=1,那么第一个出局的人就是2号,如下图,到目前为止,我们可以知道当i=1时,出局的人为:M-1

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

现在我们再来看另外一种情况,还是10个人,不过现在M变为11,i还是1,难道现在第一个出局的人还是M-1吗?11-1=10?,根本就不存在编号为10的这个人,此时应该出局的应该是编号为0的这个人,那怎么办呢? 可以这样:(M-1)%N,那么不管M=3还是M=11,都可以正确得出第一个出局的人的编号。

第一个人出局的编号是完全可以通过数学公式计算而来,无需通过递归

接下来就是比较重要的了,我们还是以N=10(总人数),M=3(报的数)这个例子来说明,初始情况为:

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

当报数报到3,出局一个之后,变为:

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

此时,这些编号已经不连续了,但是3  4  5  6  7  8  9  0  1 这些数字还是紧挨着的,且下一次报数从3开始,但是,之后的报数总要考虑原编号2处空位问题

如何才能避免已经产生的空位对报数所造成的影响呢?

可以将剩下的9个连在一起的数组成一个新的环(将1、3连接),这样报数的时候就不用在意3的空位了。但是新产生的环的数字并非连续的,这就比较麻烦了。

我们需要想一种办法来解决:我们可以将组成的新的环重新编号,怎么做呢?,我们可以从刚刚出局的人的下一个人开始从0进行编号,如下图所示

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

但是这个时候问题又来了,怎么做才能使得在新一轮的编号中按照原规则报数得到的结果推出在旧一轮中对应的数字?,我们继续看例子,现在继续在新一轮中开始报数,那么当报数到3的时候,2号出局。此时到底怎么通过2号来推出在旧一轮中应该出局的正确编号?如何由新一轮中的2得到旧一轮中的5呢?

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

新一轮中的编号:(旧一轮中的编号-最大报数值M)%旧一轮中的总人数

那么,旧一轮中的编号:(新一轮的编号+最大报数值M)%旧一轮中的总人数

接下里非常重要啦!也就是说,原序列(N个人)中第二次出局的编号可以由新序列(N-1个人)第一次出局的编号通过特定的公式运算得出。

新序列(N-1个人)的编号也是从0开始的,还是这个图:

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

针对于这个新序列(N-1个人)第二次出局的人可以由(N-2个人)的新序列的第一次出局的人通过特定的公式推出,并且(N-1个人)这个序列第二次出局的人的编号与(N个人)这个原序列中第三次出局的人的编号是有对应关系的。

这样讲大家可能还是云里雾里的,不太明白,没有关系,接下来我们举一个例子大家就都能明白啦!,我们先来看两张图,一定要重点理解这两张图

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

 数据结构 约瑟夫问题,数据结构与算法专题,数据结构

我们以第一图为例子讲解:N=10,M=3

第一步💪:当N个人时,第一个需要出局的人为2号(编号从0开始:0-9),那么剩下的序列就是

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

第二步💪:第一步出局2号之后,剩下N-1个人,将N-1个重新编号,如下:

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

此时,从新一轮编号开始重新报数,报到2的时候出局,那么我们可以通过2推算出5,从而得到N个人中第二个出局的人是编号5,怎么推算呢?

旧编号=(新编号+最大报数值M)%旧一轮的人数取余(2+3)%10=5;

第三步💪:接下来又需要新的一轮,即N-2个人

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

第四步💪:将N-2个人重新进行编号,得到下图

数据结构 约瑟夫问题,数据结构与算法专题,数据结构

第五步💪:N-2个人又要报数出局,而N-2个人第1个出局的人就是N-1个人时第二个出局的人,此时可以看出2号出局,如何通过2号推算出N-1个人出局时的第二个人的编号?

旧编号=(新编号+最大报数值M)%旧一轮的人数取余(2+3)%9=5;

所以N-1轮的时候第二个出局的人对应的编号是5号,而通过N-1第2个出局的人也就是5号,又可以推算出N个人时出局的第3个人

旧编号=(新编号+最大报数值M)%旧一轮的人数取余(5+3)%10=8;所以N个人时第3个出局的人的编号为8。

往后的步骤以此类推。

递归方法实现的约瑟夫环只有几行,但是理解起来却不简单,希望你们可以多花些功夫钻研,只有这样,才会成长

代码如下:

int ysf(int n, int m, int i)
{
	if (i == 1)
	{
		return (m - 1 + n) % n;//多走一圈,保证大于0
	}
	else
	{
		return ((ysf(n - 1, m, i - 1) + m) % n);
	}
}

int main()
{
	int n, m;
	std::cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		std::cout << ysf(n, m, i)<<" ";
	}

	system("pause");
	return 0;
}

原文链接:https://blog.csdn.net/xiaoxi_hahaha/article/details/113036281 

## 有关该类型的题可转至[约瑟夫环练习题](约瑟夫环练习题_leisure-pp的博客-CSDN博客)文章来源地址https://www.toymoban.com/news/detail-717046.html

到了这里,关于数据结构与算法——约瑟夫环的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构—约瑟夫环问题(C语言版)

    目录 首先什么是约瑟夫环 约瑟夫环实现方式 一、创建结构体变量 二、初始化链表 三、构建循环链表 四、删除链表  五、完整代码及注释讲解 约瑟夫环 是 循环链表 中的一个经典问题;题目描述:n 个人围成一圈,从第一个人开始报数,数到 m 的人出列,再由下一个人重新

    2024年02月11日
    浏览(31)
  • 数据结构学习-循环链表:处理约瑟夫环问题

    目录 问题描述 一、基本概念  1.普通链表 2.单向循环链表  二、问题处理 1.创建链表 2.查找 3.删除  4.其他  三.实验环节 四.总结 约瑟夫环问题的一种描述是:编号为1,2,...,n的n个人按顺时针方向围坐一圈,每人持有一个密码(正整数)。一开始任选一个正整数作为报数

    2024年02月07日
    浏览(29)
  • C语言数据结构-用链表解决约瑟夫环问题

    只是普通的大学生一枚,不会很牛的技巧和算法,只是在做数据结构作业中的一点感悟和思考。也不知道自己写得对不对,有什么意见和建议都可以直接指出来哦,我虚心接受(低头鞠躬.jpg)...... 试用线性表的链表存储结构来实现约瑟夫(Josephu)问题。约瑟夫问题如下:设有

    2024年02月06日
    浏览(34)
  • 数据结构实验---顺序表的合并---链表的基本操作---重点解析约瑟夫问题

    实验的写法多种多样,但本文并未采用 #define 定义容量的写法,这样写已经是很老旧过时的写法。所有实验主体采用均为动态开辟,后续如果利用 C++ 来写或许会应用更多语法… 本篇展示数据结构的两个实验 其中,重点分析约瑟夫问题 实验中代码的命名风格等均与下方博客

    2024年02月16日
    浏览(49)
  • 数据结构上机实验——栈和队列的实现、栈和队列的应用、进制转换、约瑟夫环问题

      1.利用栈的基本操作实现将任意一个十进制整数转化为R进制整数。   2.利用循环队列实现.约瑟夫环问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到k的那个人出圈;他的下一个人又从1开始报数,数到k的那个人出圈;依

    2024年02月08日
    浏览(31)
  • C语言---数据结构实验---顺序表的合并---链表的基本操作---重点解析约瑟夫问题

    实验的写法多种多样,但本文并未采用 #define 定义容量的写法,这样写已经是很老旧过时的写法。所有实验主体采用均为动态开辟,后续如果利用 C++ 来写或许会应用更多语法… 本篇展示数据结构的两个实验 其中,重点分析约瑟夫问题 实验中代码的命名风格等均与下方博客

    2024年02月16日
    浏览(35)
  • 数据结构实验1约瑟夫环

    刚开始m值为20 循环链表

    2024年02月08日
    浏览(35)
  • C语言数据结构篇——约瑟夫环的实现

    作者名:Demo不是emo  主页面链接 : 主页传送门 创作初心: 对于计算机的学习者来说,初期的学习无疑是最迷茫和难以坚持的,中后期主要是经验和能力的提高,我也刚接触计算机1年,也在不断的探索,在CSDN写博客主要是为了分享自己的学习历程,学习方法,总结的经验等

    2023年04月08日
    浏览(25)
  • 【算法】约瑟夫环问题解析与实现

    约瑟夫环(Josephus Problem)是一个经典的数学问题,涉及一个编号为 1 到 n 的人围成一圈,从第一个人开始报数,报到某个数字 m 的人出列,然后再从下一个人开始报数,如此循环,直到所有人都出列。本篇博客将详细解析约瑟夫环问题,并使用 Python 实现算法。 在约瑟夫环问

    2024年02月06日
    浏览(33)
  • 约瑟夫环问题

    n n n 个人围成一圈,从第一个人开始报数,数到 m m m 的人出列,再由下一个人重新从 1 1 1 开始报数,数到 m m m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。 注意:本题和《深入浅出-基础篇》上例题的表述稍有不同。书上表述是给出淘汰 n − 1

    2024年02月02日
    浏览(23)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包