【算法】约瑟夫环问题求解
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
"如果你不能简单地解释某一概念,说明你没有很好地掌握它。"——艾尔伯特·爱因斯坦