约瑟夫环问题的三种解决方法

  约瑟夫环问题是一道很经典的算法题,相信大家也都很熟悉,这里我们就一起来看看一看这道经典算法题的解法。
  >约瑟夫环问题的描述如下

编号为 1-N 的 N 个士兵围坐在一起形成一个圆圈,从编号为 1 的士兵开始依次报数(1,2,3…这样依次报),数到 m 的 士兵会被杀死出列,之后的士兵再从 1 开始报数,直到最后剩下一士兵,求这个士兵的编号。

方法一:数组
  采用数组来解决这道题。
  创建一个和题目人数相等的数组,然后给数组每个元素赋值1,2 ,3…n,数组每个元素就代表一个士兵。(这里假设n=8,m=3)。

  • 下面就来演示这个报数的过程: (-1表示该士兵死亡)

  • 最开始:1——2——3——4——5——6——7——8

  • 第一次报数完毕:1——2——-1 ——4——5——6——7——8

  • 第二次报数完毕:1——2——-1 ——4——5 ——-1——7——8

  • 第三次报数完毕:-1——2——-1 ——4——5——-1 ——7——8

  • 第四次报数完毕:-1——2——-1 ——4——-1——-1——7——8

  • 第五次报数完毕:-1——-1——-1 ——4——-1 ——-1——7——8

  • 第六次报数完毕:-1——-1——-1 ——4——-1——-1——7——-1

  • 第六次报数完毕:-1——-1——-1 ——-1 ——-1——-1——7——-1

最后只剩下7,也就是活下来的士兵是7号。
 这种方法思路挺简单的,不过编码也不是那么的容易。在编程过程中,要考虑数组元素是否为-1,还要控制遍历数组的临界点,特别是到了数组末尾必须重新设置下标为0重新遍历,直到最后数组中只剩下一个不为-1的元素。

下面就给出核心的算法代码

void Solution1(short* Array , int n,int m)
{
	int count = 1;						//报数标记
	int nums = 0;						//记录死掉士兵的个数
	for (int i = 0; i < n; i++)
	{
		if (Array[i] != -1 && count == m)	//报到m的士兵被杀死
		{
			count = 1;
			Array[i] = -1;
			nums++;
		}
		if (Array[i] != -1)
			count++;
		if (i == n - 1)						//到数组末尾重新设置下标为0
			i = -1;
		if (nums == n - 1)					//只剩下最后一个士兵则退出循环
			break;
	}
}

下面是主函数


#include<iostream>
using namespace std;

const int NUM = 1000;				//最大数量的士兵
int main()
{
	short Array[NUM];
	//法一:用数组方式解决
	for (int i = 0; i < NUM; i++)
		Array[i] = i+1;
	int n, m;			//n是士兵数量,m是被杀的序号
	cin >> n >> m;
	Solution1(Array, n, m);
	for (int i = 0; i < n; i++)
	{
		if (Array[i] != -1)
			cout << "剩下的士兵编号是:"<<Array[i]<<endl;
	}
}
这种做法的时间复杂度是 O(n * m), 空间复杂度是 O(n);

方法二:环形链表
  一般我们都是在学习链表的时遇到约瑟夫环问题的,所以这道题多数人会采用链表的方式去解决的。
  链表和数组其实很类似,只不过在表示杀死士兵的时候,直接删除这个节点就可以了,直到最后链表只剩下一个节点,那么这个节点的val值就是最后存活士兵的编号。

这里就不详细演示具体过程了,直接贴代码,过程和上面的数组法类似。

节点类
class List_Node
{
public:
	using  pList_Node = List_Node* ;
	int val;				//士兵编号
	pList_Node next;

public:
	List_Node()
	{
		this->val = 0;
		this->next = nullptr;
	}
	List_Node(int val)
	{
		this->val = val;
		this->next = nullptr;
	}
};
解决方案类
class Solution
{
public:
	int Solution1(List_Node::pList_Node head, int m , int n)
	{
		int count = 1;
		List_Node::pList_Node Cur = head;
		List_Node::pList_Node pre_node = nullptr;		//前驱节点

		if (m == 1 || n < 2)
			return n;
		while (Cur->next != Cur)
		{
			// 删除节点
			if (count == m) 
			{
				count = 1;
				pre_node->next = Cur->next;
				Cur = pre_node->next;
			}
			else 
			{
				count++;
				pre_node = Cur;
				Cur = Cur->next;
			}
		}
		return Cur->val;
	}

	//创建链表
	List_Node::pList_Node Create_List(int n)
	{
		List_Node::pList_Node head = new List_Node(1);
		List_Node::pList_Node last = head;
		for (int i = 1; i < n; i++)
		{
			List_Node::pList_Node nNode = new List_Node(i+1);
			last->next = nNode;
			nNode->next = head;
			last = nNode;
		}
		return head;

	}
	
};

主函数
#include<iostream>
using namespace std;
int main()
{
	//创建链表
	int n, m;
	cin >> n >> m;
	Solution solution1;
	List_Node::pList_Node head = solution1.Create_List(n);
	cout << "剩下的士兵编号是:" << solution1.Solution1(head, m , n);
	return 0;

}
这种做法的时间复杂度是 O(n * m), 空间复杂度是 O(n);

那么有没有一种更为简洁的方案呢?答案是有的,下面就来揭晓(一行代码就可以搞定

方法三:递归
  相信不少人都不会想到可以用递归的思想去解决这道题。笔者之前也不知道可以用递归的思想来处理这道题,没想到用递归一行代码就可以搞定,简直不要太过分。(哈哈)
  学过算法的道友都应该直到,递归的关键就是要找到前后的的递推关系。那么就这道题而言的话,就是要找到杀死某个士兵前后每个士兵编号映射关系。
  我们定义递归函数 fun(n,m) 的返回结果是存活士兵的编号。
  当 n = 1 时,f(n, m) = 1。
  现在就是要找到 f(n,m) 和 f(n-1,m) 之间的关系。我们假设士兵数为 n, 报数到 m 的人就被处死。则刚开始的编号为

1  2   3  4  5 …m-2   m-1   m   m+1   m+2   m+3 …n

删除一个结点之后结点编号将会发生变化,现在只剩下 n - 1 个士兵。且士兵死亡前编号为 m + 1, m + 2, m + 3 的士兵成了编号为 1, 2, 3 的节点。

假设 pre 为删除之前的节点编号, now 为删除了一个节点之后的编号,则 pre 与 now之间的关系为 pre = (now + m - 1) % n + 1。

  • 这里强调下为啥要+1,因为编号是从1开始的,这里避免出现pre==0的情况,所以+1
    这样我们就得到的士兵死亡前后每个士兵的编号情况,下面就可以写这个递归函数了
int fun(int n, int m) 
{
	return n == 1 ? n : (fun(n - 1, m) + m - 1) % n + 1;
}

确实这一行代码就可以搞定,而且他的性能更加的好

这种做法的时间复杂度是 O(n ), 空间复杂度是 O(n);
写在最后

  如果那你觉得这篇文章对你有所帮助的话,请给笔者点个 ,让更多的读者看到这篇文章。先在这里感谢各位道友啦~~~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值