第六天:约瑟夫问题(环链+模拟+递归)

本文深入探讨约瑟夫问题的三种解法:数组、环链表及递归,对比不同算法的时间复杂度,揭示递归法如何将时间复杂度降至O(n)。

今天是释然发题解的第六天,以后每一天都会和大家分享学习路上的心得,希望和大家一起进步,一起享受coding的乐趣。
本文约2500字,预计阅读15分钟
昨天我们学习了链表,忘记的小伙伴们可以看一下哦:

初识链表

学完了链表,那么赶紧操作一下。今天我们来聊一聊的链表的相关题目,明天和大家分享二分和贪心的相关知识:

题目来源:洛谷P1996

题目链接
题目描述
nn 个人围成一圈,从第一个人开始报数,数到 mm 的人出列,再由下一个人重新从 11 开始报数,数到 mm 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。

输入格式
输入两个整数 n,m。

输出格式
输出一行 n 个整数,按顺序输出每个出圈人的编号。

输入输出样例
输入 #1
10 3
输出 #1
3 6 9 2 7 1 8 5 10 4
说明/提示
1 ≤ m , n ≤ 100

解题思路1:数组

那么一看这个题,我们知道链表对插入和删除元素有奇效,很容易想到用链表来解决这个问题,但是转念又想,我们开一个标记的数组好像也能解决这个问题:
于是我尝试了不用链表的方法:
我们先定义一个标记的数组

#include<cstring>
bool p[101];
memset(p,1,sizeof(p));//初始化
//用一个for循环来进行模拟
for(int i=1;;i++)
    {	if(p[i]==1)//说明这个人还在
        k++;
        if(k==m)
        {
            p[i]=0;//把这个人踢出去
            cout<<i<<" ";num++;
            if(num==n)return 0;//判断结束条件
            k=0;//计数的人数恢复
        }
        if(i==n)
        i=0;//循环到头了
    }

完整题解:C++代码

#include<iostream>
#include<cstring>
using namespace std;  
int n,m,k,num;
int a[101];
bool p[101];
int main(void)
{	cin>>n>>m;
	memset(p,1,sizeof(p));
	for(int i=1;;i++)
	{	if(p[i]==1)
		k++;
		if(k==m)
		{
			p[i]=0;
			cout<<i<<" ";num++;
			if(num==n)return 0;
			k=0;
		}
		if(i==n)
		i=0;
	}
}

时间复杂度为O(n*m)

解题思路2:环链

既然学习了链表,那么碰到这种插入和删除的问题肯定也是能解决的
我们先开一个链表,让每个结点存储的元素为i,也就是第i个结点存储i这个数据

//我们先写一个创建结点的结构
struct node
{
	int data;
	struct node *next;	
};
//创建一个头结点
	struct node *head,*p,*q;
	head=(struct node *)malloc(sizeof(struct node));//动态分配内存
	head->data=1;
	head->next=head;
	q=head;
//用循环来创建多个结点
	for(i=2;i<=n;i++)
	{
		p=(struct node *)malloc(sizeof(struct node));//动态分配内存
		p->data=i;
		q->next=p;
		p->next=head;
		q=p;
	}
	q=head;
//开始查找,并且删除元素
left=n;//还剩多少人
do{
	i=1;
	while(i<m)
	{
		p=q;
		i++;
		q=q->next;
	}
	cout<<q->data<<" ";f
	left--;
	p->next=q->next;
	free(q);
	q=p->next
}while((left!=0)

然后写到这里以为就完事了嘛?
我们来看一下这种算法的时间复杂度:查找m次,删除一个元素,总共删除n次,时间复杂度为O(n*m)
作为我这样的初学者觉得已经很满足了,但是大佬就是大佬,我看了一下别人写的代码发现时间复杂度可以降为O(n)

解题思路3:递归

啊?什么递归?
没错,这个规律一不好找,二容易找错,太厉害了,还是值得多去揣测一下的

我们可以给环形链表的节点编号,如果链表的节点数为 n, 则从头节点开始,依次给节点编号,即头节点为 1, 下一个节点为2, 最后一个节点为 n.

我们用 f(n) 表示当环形链表的长度为n时,生存下来的人的编号为 f(n),显然当 n = 1 时,f(n) = 1。假如我们能够找出 f(n) 和 f(n-1) 之间的关系的话,我们我们就可以用递归的方式来解决了。我们假设 人员数为 n, 报数到 m的人就自杀。则刚开始的编号为

m - 2

m - 1

m

m + 1

m + 2

进行了一次删除之后,删除了编号为m的节点。删除之后,就只剩下 n - 1 个节点了,删除前和删除之后的编号转换关系为:

删除前 — 删除后

… — …

m - 2 — n - 2

m - 1 — n - 1

m ---- 无(因为编号被删除了)

m + 1 — 1(因为下次就从这里报数了)

m + 2 ---- 2

… ---- …

新的环中只有 n - 1 个节点。且编号为 m + 1, m + 2, m + 3 的节点成了新环中编号为 1, 2, 3 的节点。

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

注:有些人可能会疑惑为什么不是 old = (new + m ) % n 呢?主要是因为编号是从 1 开始的,而不是从 0 开始的。如果
new + m == n的话,会导致最后的计算结果为 old = 0。所以 old = (new + m - 1) % n + 1.

这样,我们就得出 f(n) 与 f(n - 1)之间的关系了,而 f(1) = 1.所以我们可以采用递归的方式来做。

代码如下:

//时间复杂度为O(n)
public static Node josephusKill2(Node head,
int m) {
if(head == null || m < 1)
return head;
int n = 1;//统计一共有多少个节点
Node last = head;
while (last.next != head) {
n++;
last =ast.next;
}
//直接用递归算出目的编号
int des = f(n, m);
//把目的节点取出来
while (–des != 0)
{ head = head.next;
}
head.next = head;
return head;
}
private static int f(int n, int m) {
if (n == 1) {
return 1;
}
return (f(n - 1, m) + m - 1) % n + 1;
}

好了,今天的有关约瑟夫问题的知识就到这里。
释然每天发布一点自己学习的知识,希望2年后我们也能在ACM的赛场上见面,一起去追寻自己的程序猿之路吧!

后期也会和大家一起分享学习心得和学习经验呢,明天我们不见不散哦!

下期预告:

链表的相关题目

如果大家有什么建议或者要求请后台留言,释然也想和大家一起进步呀!
联系方式:shirandexiaowo@foxmail.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shirandexiaowo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值