约瑟夫环问题(c语言实现)

问题描述

有N个人,编号为 1~n,围成一个圈,编号1的人手上有一个热狗开始向下一个人传递,在热狗传递 m 次后,此时拿着热狗的人退出,剩余的人围成一个圈,从退出的人的下一位再开始上述循环,知道最后一个留下来的人胜利。

如果n为5,m为1,则退出的人编号依次为2,4,1,5,最后胜利的是3。

算法

1. 非递归法

这种方法是目前找到的最优算法,时间复杂度为 O ( N ) O(N) ,因为没有用递归,不需要额外的空间。

算法介绍参考:约瑟夫环非递归算法分析

算法理解

  1. 实际玩家编号是1~n,但为了计算操作方便使用编号0~n-1,原因如下:

m可能大于n,将编号改为0~n-1,则第一次退出的编号可以记为:m%n,因为m%n范围是0~n-1,如果n为5,m为5,则退出的是0,最后计算出最终的胜利者后只需将编号加1就是实际编号了。

  1. 第一次退出的玩家编号是 m%n,则下一位的编号k为 m%n+1,因此新的环为:
    k , k + 1 , k + 2 , , n 1 , 0 , 1 , 2 , , k 2 k,k+1,k+2,\cdots,n-1,0,1,2,\cdots,k-2
    新一轮循环开始后,重新编号,上述编号映射为:
    0 , 1 , 2 , , n 2 0,1,2,\cdots,n-2
    共有 n-1 个人。

可得出上述映射关系,新编号 x 对应旧编号 (k+x)%n.

  1. 运用递归思想求解
    假设玩家人数是 n-1 时赢家编号是 x,则由上述可得玩家编号是 n 时,赢家编号是 (k+x)%n,将 k = m%n+1 带入得到:

(x+m%n+1)%n = (x%n + (m%n)%n + 1%n)%n

因为x小于n,m%n 小于n,当 n>1 时,1%n 为1,因此 n > 1 时得到:

(x+m%n+1)%n,进一步得到 (x+m+1)%n。

当 n=1 时,只有编号0,因此赢家为编号0,对应实际为编号1。
想要求x,相同的办法要知道 n-2 时的赢家,则要知道 n-3 时的赢家,最终可根据 n 为1时求解,因为对应的映射公式相同。

假设人数为 i-1 时赢家编号为 f(i-1),则人数为 i 时赢家编号为 f(i) = ((f(i-1) + m + 1) % i。

程序

/*n = 1 和其他情况分开讨论,但在下面式子中,n为1时结果为1,
符合,因此合并到一起
*/
int josephus(int n, int m)
{
	int i, temp = 0;
	for (i = 1; i <= n; i++) 
		temp = (temp +  m + 1) % i;
	return temp+1;
}

2. 递归法

递归思想与上述一样,程序改为递归形式:

程序

int josephus(int n, int m)
{
	if (n = 1)
		return 0;
	else
		return (josephus(n-1,m) + m + 1) % n;
}

最后将求出的结果加1

3. 循环双链表

思路

实现方法参考:
约瑟夫环双向链表
进一步优化方法:

  1. 令 m’ = m%n,这样如果 m 大于或等于n时可以避免多走一个循环。
  2. 当 m’ > n/2 时,可以选择从另一个方向遍历,节约时间,因此需要用双链表。

思路:

  1. 输入参与游戏人数n和传递次数m,记录数据为 0~n,则从当前节点开始,删除之后的第 m 个节点。

  2. 建立循环双链表,带头节点,头节点指向首节点,尾节点指向首节点。
    为了让插入首节点和中间节点操作一致,加入首节点,创建链表时让尾节点指向首节点,链表创建完成后,将尾节点和首节点相连,头节点指向首节点。

  3. 链表建立后查找往后的第m个节点,每次循环后人数减1,然后令 m = m%n。判断 m 是否大于 n/2,大于则逆向查找,否则顺序查找。

    另外,删除节点时判断是否是首节点,删除首节点需要改变头节点指向地址。

程序

#include<stdio.h>
#include<stdlib.h>
struct node;
typedef struct node *ptrtonode;
typedef ptrtonode list;
typedef ptrtonode position;
struct node {
	ptrtonode prior;
	int data;
	ptrtonode next;
};
int input(void);
list InitList(void);
void CreatList(list L, int n);
int Josephus(list L, int n, int m);
void DeleteList(list L);
int main(void)
{
	int n, m, s;
	list L;
	printf("Please enter the number of people: ");
	n = input();
	printf("Please enter the number of m: ");
	m = input();
	m = m % n;//避免一次游戏时循环几次
	L = InitList();
	CreatList(L,n);
	s = Josephus(L,n,m);
	printf("\nThe winner is %d\n",s);
	DeleteList(L);
	return 0;
}
int input(void)
{
	int n, status, ok = 1;
	while (ok) {
		status = scanf("%d",&n);
		if (status != 1 || n < 1 || getchar() != '\n') {
			while (getchar() != '\n')
				continue;
			printf("Enter again (n > 0): ");
			continue;
		}
		return n;
	}
}
//链表初始化,创建头节点,头指针
list InitList(void)
{
	list L = (list)malloc(sizeof(struct node));
	if (L == NULL) {
		fprintf(stderr,"Out of space\n");
		exit(EXIT_FAILURE);
	}
	L->prior = NULL;
	L->next = NULL;
	return L;
}
void InsertTail(position p, position h, int n)
{
	list tem = InitList();
	tem->data = n;
	p->next = tem;
	tem->prior = p;
	tem->next = h;//每次让尾节点指向头节点
	h->prior = tem;
}
/*建立循环链表,带有头节点,为了让插入第一个节点和其他节点
操作一致。
先让尾节点指向头节点,链表建立完成后,再让尾节点指向首节点。
*/
void CreatList(list L, int n)
{
	int num;
	position p = L, head = L;//head指向头节点
	for (int i = 0; i < n; i++) {
		InsertTail(p,head,i);
		p = p->next;
	}
	//链表创建完成让首节点和尾节点相连
	position pfirst = head->next, pend = head->prior;
	pfirst->prior = pend;
	pend->next = pfirst;
}
int IsFirst(position p, position h)
{
	return h->next == p;
}
position DeleteNode(position p)
{
	position pbefore = p->prior, pafter = p->next;
	pbefore->next = pafter;
	pafter->prior = pbefore;
	free(p);
	return pafter;
}
position AntiDelete(position p, position h, int m)
{
	for (int i = 0; i < m; i++) 
		p = p->prior;
	//判断要删除的节点是不是首节点,
	//删除首节点要改变头节点指向地址
	if (IsFirst(p,h)) 
		h->next = p->next;
	return DeleteNode(p);
}
position Delete(position p, position h, int m)
{
	for (int i = 0; i < m; i++)
		p = p->next;
	if (IsFirst(p,h))
		h->next = p->next;
	return DeleteNode(p);
}
int Josephus(list L, int n, int m)
{
	position h = L, p = h->next;
	int num = n;
	while (p->next != p) {
	//如果 m 大于人数的一半,逆时针删节点
	//顺时针移动m,则逆时针移动num-m
		if (m > (int)num/2) 
			p = AntiDelete(p,h,num-m);
		else
			p = Delete(p,h,m);
		num = n-1;
		m = m % num;
	}
	//退出时说明只剩一个节点(不包括头节点)
	//将最后的节点数据加1为实际编号
	return p->data + 1;
}
void DeleteList(list L)
{
	position p = L->next, tem;
	L->next = NULL;
	L->prior = NULL;
	while (p->next != p) {
		tem = p->next;
		tem->prior = p->prior;
		p->prior->next = tem;
		free(p);
		p = tem;
	}
	free(p);
}

测试结果:

Please enter the number of people: 5
Please enter the number of m: 1

The winner is 3
发布了120 篇原创文章 · 获赞 2 · 访问量 5792

猜你喜欢

转载自blog.csdn.net/Lee567/article/details/103641007