【算法】约瑟夫环问题求解

0.约瑟夫环问题描述

已知n个人(编号1,2,3,…,n表示)围坐在一张圆桌周围。从编号为1的人开始报数,数到k的人出列;与他相邻的下一个人又从1开始报数,数到k的人又出列;依此规律重复,直到所有人出列,求最后一个出列的人。

1.模拟数组解法

思路:将所有元素标识初始化为0,每次将报到k的值置为1,下一轮不再加入计数。
使用sum计数,每次移除一个,sum加一,直到sum为n-1时退出循环。

// 模拟数组解法
int josephus_solver(int a[], int size, int k)
{
	// 数组无效时,返回-1作为提示,因为数组中的元素均为正数
	if (a==NULL || size<=0)
		return -1;

	// i为数组索引
	int i = 0;

	// 用一个removed数组作为每个元素是否被移除的标识,1表示移除
	int *removed = (int *)malloc(sizeof(int)*size);
	memset(removed, 0, sizeof(int)*size);

	int j = 0;	 // 用来模拟报数
	int sum = 0; // 统计被移除的元素数量,当只剩一个元素时终止
	i = 0;
	while (sum<size-1)
	{
		while (removed[i]==1) i++;
		if (i==size) i = 0;
		// 报数报到k并且当前的元素尚未被移除,则将其移除
		if (removed[i]!=1 && j%k==0) {
			printf("remove a[%d]=%d\n", i, a[i]);
			removed[i] = 1;
			sum++;
		}

		if (++j==k+1) j = 1;
		if (++i==size) i = 0;
	}

	for (i=0; i<size; i++)
		if (removed[i])
			break;

	// 释放内存资源
	free(removed);
	// 指向NULL,防止成为野指针
	removed = NULL;

	return a[i];
}

2.递推求解

详细递推过程如下
已知队里有n个人,编号为: 0, 1, 2, ..., n-1
进行一轮报数,报到k的人出队,第一次出队的人编号必为(k-1)%n
这时我们可以把队伍编号记为: 0, 1, 2, ..., k-2, k-1, k, ..., n-1
将k-1标记为X,表示已经出队。 0, 1, 2, ..., k-2, X , k, ..., n-1
重新编号让k为0,则有: n-k, n-k+1, n-k+2, ..., n-2, (X), 0, 1, ..., n-1-k

此时的编号与前一轮之间的关系不难看出来,记前一轮为index[n],表示前一轮中共有n个人,编号为0~(n-1)
当前的编号记为index[n-1],因为相比于前一轮被移除一个,并从k重新编号0~(n-2)
则编号之间的对应关系:index[n]=(index[n-1]+k)%n

我们可以一直推下去:有n-2人,有n-3人, ..., 有1人。
这样就得到了递推式:index[n] = (index[n-1]+k)%n
当只有1人时:index[1] = 0

前一轮的编号=(新一轮的编号+k)%前一轮的人数
也可以理解为:新一轮编号向前移动k位再通过取模形成环即可得到前一轮的编号。
我们很容易知道最后一个出队的人的编号一定是0,因为此时只剩下他一个人。再通过上述的递推关系式,我们就能够得到他对应的第一轮的编号

// 递推求解
// 时间复杂度为O(n),空间复杂度为O(1),只能求得最后出局的人。
int josephus_solver_recursive(int a[], int size, int k)
{
	// 数组无效时,返回-1作为提示,因为数组中的元素均为正数
	if (a==NULL || size<=0)
		return -1;

	// 最后一个出队的人编号一定是0,因为此时只剩下他一个人
	int index = 0; // 当只有1人时编号为0的人最后出列
	for (int i=2; i<=size; i++) {
		index = (index+k)%i;
	}

	return a[index];
}

3.链表解法

类似于数组法,不过不适用辅助标志数组,而是直接使用链表来表示剩余的人,只剩一个时即是最后的结果。

// 链表解法
#include <list>

int josephus_solver_list(int n, int k)
{
	std::list<int> ltmp;
	for(int i=1; i<=n; ++i)
		ltmp.push_back(i);
	auto pos = ltmp.begin();
	while(ltmp.size() > 1) {
		for(int i=1; i<k; ++i) {
			++pos;
			if(pos==ltmp.end())
				pos = ltmp.begin();
		}
		pos = ltmp.erase(pos);
		if(pos==ltmp.end())
			pos = ltmp.begin();
	}
	return ltmp.front();
}

4.LeetCode题目

LeetCode上的约瑟夫环问题 1823-findTheWinner


"如果你不能简单地解释某一概念,说明你没有很好地掌握它。"——艾尔伯特·爱因斯坦

posted @ 2022-03-04 15:00  coffee_tea_or_me  阅读(172)  评论(0编辑  收藏  举报