[C & C++]利用循环链表解决约瑟夫环问题

约瑟夫环问题重述 Problem Details

问题源于一个故事,讲述者据传为著名犹太历史学家Josephus。

罗马人占领乔塔帕特,39个犹太人外加Josephus和他的朋友共计41人躲进一个洞窟,为了躲避敌人的抓捕,决定集体自杀。自杀的规则是所有人围成一个圆圈,确定一个数字从第一个人开始报数,报到数的人自杀,从下一个人开始继续。然而Josephus和朋友反对,于是假装同意。通过将自己和朋友安排在第16和31的位置,避免了死亡的遭遇。

以上故事衍生成了一个经典的数学问题。法国数学家在《数目的游戏问题》提出了类似的问题。
综合各种形式,给出一种最常见的题目形式:

n个人围成一圈,从第一个人开始报数,第m个出局,游戏从下一个人继续,直到最后一人存活,游戏结束。试问每个人的出局顺序。

分析 Analysis

本题具有很多种解法,排除各种编程语言的差异,解法之间的主要区别是选用的数据结构不同。因此,本题分析的入手点即应当选择什么数据结构较优(或者说更顺手更易于理解)

本题可以采用的数据结构非常多,见诸于网络的有数组法循环队列法循环链表法等等,利用递归思想10行之内解决问题的大神也不少见。在这么多的方法中,分析题目可以很容易发现,所有的“人”,也就是各个数据单元,围成了“一圈”,进行循环报数。由此,关键词获得,循环首尾相接,很容易想到“循环队列”或者“循环链表”。循环链表与循环队列实质上是一种数据结构。这里我们尝试选用循环链表解决问题。

那么使用循环链表是否合适呢?
首先,相对于其它数据结构,其本来形式更直观地对应了题目的实际形象;
其次,循环链表可以很容易地构成“循环”;
最后,循环链表进行删除操作相对容易,更方便将其中一个结点删除而不破坏整个数据结构。
综上,使用循环链表是合适的。

创建循环链表 Create a Circular List

C

首先建立一个结点 node,同时因为本题对象只有 Josephus 环中的人,因此直接定义一个结点类型 person:

typedef struct node
{
	int number;
	struct node *next;
}person;

接下来就利用上面定义的 person 结点创建循环链表结构 CirList,int型变量n用于接收链表的结点数:

person *CirList( int n )
{
	person *head = (person *)malloc(sizeof(person));//Create the head node
	head->number = 1;	//The 1st person is 1.
	head->next = head;	//Make head connects to itself making it the Circular List.
	
	person *BodyCreator = head; 	//Set a pointer to help make the body
	int i;				//Every i is a person node coming after the head node
	for (i = 2; i <= n; i++)
	{
		person *body = (person*)malloc(sizeof(person));
		body->number = i;	//Keypoint~Cause its the person after No.1 so he starts from 2
		body->next = NULL;

		body = BodyCreator->next;	//Attach the body to the CirList
		BodyCreator = BodyCreator->next;
	}
	BodyCreator->next = head;	//After all the person nodes are created, attach the BodyCreator to the head to make a circle
	return head;
}

在上述程序中,有如下几点需要指出:
1.将头指针head指向的头结点直接赋值1,是出于题目考虑:

head->number = 1;

这样做有两个原因,其一是使得第一个头结点就是编号为1的人,其二是最后一个人的next指向的头结点应为编号为1的头结点,若习惯性定义单链表的方式将头结点的number存储下一结点的地址的话,将出现错误,无法构成循环。

2.指针BodyCreator用于指向待插入新结点的结点位置,看似累赘,实则起到了尾指针的作用,相对的body结点则是较为独立的新结点,将其插入到BodyCreator后成功入链表。

3.离开计数循环后,将BodyCreator的后继指向头指针至关重要,这保证了“循环”二字:

BodyCreator->next = head;

C++

C++是面向对象的语言,类的使用是C++的特质,因此直接定义两个类,结点类CirListNode和循环链表类CirList:

class CirListNode
{
	public:
	CirListNode()
	{
		next = 0;
	}
	CirListNode( int el, CirListNode *ptr = 0 )
	{
		info = el;
		next = ptr;
	}

	int info;
	CirListNode *next;
	
}

class CirList()
{
public:
	CirList();
	~CirList();
	int isEmpty()
	{
		return head == 0;
	}
	void addToHead(int);
	void addToTail(int);		//Use it to form the CirList
	int deleteFromHead();
	int deleteFromTail();
	void deleteNode(int);
	void Traverse();			//Traverse the CirList
	void JosephOut(int);		//Core Function
private:
	CirListNode *head;
	CirListNode *tail;
}

对上述.h文件中的重要部分列出具体代码:

  1. 循环链表的构造函数:
CirList::CirList()
{
	head = new CirListNode();
	head->info = 1;				//Same as C version
	head->next = head;			//Make a circle
	tail = head;				//Must give a value to tail
	tail->next = head;			//Make a circle
}

此处将head的 info 设为1的目的是将第一个人定位在head处,这样tail的下一个结点head能够直接衔接上,否则这是一个废结点。

  1. 循环链表的析构函数:
CirList::~CirList()
{
	delete (head);
}
  1. 利用尾插法完成构建循环链表,相对于在C语言中使用BodyCreator的麻烦,CirList 中的私有tail尾指针很好地充当了标记插入位置的功能:
CirList::AddToTail(int el)
{
	if(isEmpty())
	{
		tail = new CirListNode(el);
		tail = tail->next;
	}
	else
	{
		tail->next = new CirListNode(el, tail->next);	//Create a new node first, then attach it to the positon after tail
		tail = tail->next;	//Move tail to the next node
	}
}
  1. 遍历整个循环链表并输出,只遍历一次到尾指针为止:
CirList::Traverse()
{
	CirListNode *tmp = head;
	cout << tmp->info << "  ";
	tmp = tmp->next;
	for(tmp->next != head )
	{
		cout << tmp->info << "  ";
		tmp = tmp->next;
	}
}

这里代码略显“累赘”,读者看到这肯定容易茫然 for 循环前的输出移动是什么鬼。首先 tmp 是标记指针,希望它遍历整个链表。根据之前设计,head结点存储了第一个人的编号1,必须输出。而 for 循环的条件是当 tmp 的下一个结点不是头结点head时进行遍历,这时会发现如果不提前输出 head 的值并移动到下一结点的话,则循环永远不会进行,因为循环的起点就是tmp = head~。

有的读者认为可以直接定义 CirListNode *tmp = head->next来避免不能进入循环,但是头结点仍然没有得到输出,并不能因此就简化了代码,同时,当循环链表为空时,tmp就是空指针将会报错,那么判空语句也就不可避免。因此,目前看来这段代码并不是“多此一举”。

核心解法 Core Solution

Joseph Ring in C

虽然C和C++在实现该部分的时候思路几乎完全一致,但是因为是分两次进行问题分析的,在变量名、指针名等等方面会有细微区别,所以还是分开单独进行分析。

定义指针 p,用其记录报数的人,同时定义指针 tail 来标记指针 p 的前驱,作为删除 p 指向结点的标志:

void JosephOuto(person *head, int start, int out)
{
	person *tail = head;
	person *p = head;
	while(p->number != start)
	{
		tail = p;		//Keypoint,don't forget it
		p = p->next;	//Move p to the start number
	}
	while(p->next != p)
	{
		for( int i = 0; i < out; i++)
		{
			tail = p;	//Have to highlight again
			p = p->next;
		}
		tail->next = p->next;	//delete *p
		printf("The person who is out is: %d .\n", p->number);
		free(p);				
		p = tail->next;
	}
	printf("The last person who is out is: %d .\n", p-> number);
	free(p);
}

在上述代码中,引入 start 可以使整个功能更加完善,可以决定淘汰开始的人不一定是编号为1的人。tail 指针在编写过程中要格外留意不能赋值出错,最简单的方法就是在每次移动 p 指针前使 tail = p即可完美解决。

Joseph Ring in C++

与C语言的实现思想几乎一致,为了省事省去了设定开始位置的功能(事实是也没省几行代码),不过还是在设计标志位的时候多了一点考虑。设置旗标 flag 指针用于标识报数的人,out 用于标识 flag 指针的前驱。与使用C语言的版本相比,循环链表设置尾指针 tail 的优势得到体现。将 out 的起始位置设定在 tail 处,flag 从 head 出发,自然 out 就是 flag 的前驱:

CirList::JosephOut(int v)
{
	CirListNode *flag = head;
	CirListNode *out = tail;
	while(flag->next != head)
	{
		for (int i = 1; i < v; i++)	//Every time one person is out, flag just move (v-1) times
		{
			flag = flag->next;
			out = out->next;
		}
		out->next = flag->next;
		cout << "The person who is out is: " << flag->info << endl;
		free(flag);
		flag = out->next;
	}
	cout << "The last person who is out is: " << flag->info << endl;
	free(flag);
	free(out);
}

这里可以插入一个小花絮,在初次写这段代码时,因为设计标志时做过多次修改,因此标志的命名就显得不大合理。在上述代码中,标志报数人的指针命名为 out 更佳,标志前驱的指针命名为 flag 更佳~正好相反了哈哈。

测试图例

这里给出C++版本的运行图例,经过作者掐着手指算下来没有错误嘿嘿:
C++版本

总结 Conclusions

整个问题的解决并没有很复杂,核心在于选择合适的数据结构,关于合适的定义,既可以是自己最顺手最熟悉的,也可以是问题最直观展现出来的。

约瑟夫环问题的一个最易理解的解决方法就是使用循环链表。在实际解决过程中在寻找每轮出局位置和删除该结点的操作中有所把握,问题就迎刃而解了。

CSDN博客上的大神相当多,敝人参考过诸多前例,例如几行就解决的递归思想的代码,也有巧妙设计数组解决的。相信这些方法也能在作者的抛砖引玉下显得更为精巧和有趣。也希望各位能够轻松读懂这篇文章的内容~

写在最后

作者水平有限,时间并未非常富余。

代码编写过程中,效率和整洁度未能做到完美,非常欢迎各位评论和指点。

文章码字较为辛苦,语句斟酌未必足够通顺,编写系统也未如意,某些指针需要用到* 符号,连续使用会影响文章格式,特此声明~

本文源码以及工程文件资源已打包上传,正在审核中,链接后续补充,欢迎各位下载参考并提出宝贵意见~不胜感激

  • 3
    点赞
  • 0
    评论
  • 19
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页

打赏作者

紫峰的白色彗星

菜鸟能懂大佬不嫌弃的文字~

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值