【C++】约瑟夫问题链表解法

题目部分(来自洛谷P1996 约瑟夫问题

P1996 约瑟夫问题

题目描述

n n n 个人围成一圈,从第一个人开始报数,数到 m m m 的人出列,再由下一个人重新从 1 1 1 开始报数,数到 m m m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。

注意:本题和《深入浅出-基础篇》上例题的表述稍有不同。书上表述是给出淘汰 n − 1 n-1 n1 名小朋友,而该题是全部出圈。

输入格式

输入两个整数 n , m n,m n,m

输出格式

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

输入输出样例 #1

输入 #1
10 3
输出 #1
3 6 9 2 7 1 8 5 10 4
说明/提示

1 ≤ m , n ≤ 100 1 \le m, n \le 100 1m,n100


本文主要是思考在解题时候的一些小细节

以下是我的两种思路,先把答案贴在这里:

#include<iostream>
#include<list>
using namespace std;

const int N = 110; 

int ne[N];  //只需要next数组去储存指针信息


int main()
{
	int n,m;
	cin >> n >> m;
	for(int i=1;i<=n;i++)
	{
		ne[i] = i+1;
	}
	ne[n] = 1; //到尾巴后循环到下标为1的节点,为了计数方便,我们全程忽略了0节点 
	
	int count = 0;
	
	//错误想法:这样的循环永远停不下来,因为我们数到第m个节点后要删除它,删除这个节点是伪删除,并不是像动态分配那样释放内存彻底删除 
	//正确思考方向:考虑删除的逻辑到底是什么,在什么情况下删除会出bug,考虑特殊情况分类讨论!!!
	for(int i=1;i;i=ne[i]) 
	{
		count++;
		if(count == m-1)
		{
			if(ne[i] == ne[ne[i]]) //就剩下一个节点 
			{
				cout << i << " "; 
				break;			
			}
			
			cout << ne[i] << " ";
			ne[i] = ne[ne[i]]; //这是删除逻辑,跳过下一个节点,只剩下两个节点时执行这个逻辑不会出错,会减少成一个节点。但是一个节点执行这个逻辑不会减少成0个节点,无法跳出for循环的限制,所以要去处理这种特殊情况。 
			count = 0;
		}
		
	}
	return 0;
}
 

首先是用来存储数据的数组有没有必要创建?答案是没有必要创建,因为整个输入的数据都是从1到N连续的正整数,所以我们没必要创建element数组去保存数据,我们在循环时的下标就是对应的数据。

这时候不理解没关系,要怀着一种不求甚解的态度,只要开始着手实现代码就立马能理解了。其实这就是思维太过于空想,正如华罗庚所说要数形结合,一定要多画图去理解题目,要整理好思路再去实现代码,你可以思考花半个小时,写代码十分钟写完,这比坐那里边空想边写代码要好很多,光靠脑子想一个小时可能也写不出来。刚开始接触算法题也不要害怕浪费时间,刚开始学一样东西,慢就是快

这题要用循环链表来做,所以可以看到我在初始化完成后,将数组的最后一个节点的后继指针指向了下标为1的节点(非头结点)ne[n] = 1;

for(int i=1;i;i=ne[i])在写这个循环的时候我感觉可能会死循环,事实确实是这样,没有任何一个节点的后继指针是0,因为这是一个循环链表。这里就要注意了,等会要去处理如何跳出循环。

注意在单链表中想要删除当前位置的节点是需要牺牲时间的,所以我们提前在m-1处停下,执行删除下一个节点的逻辑,此时时间复杂度为O(1)。

ne[i] = ne[ne[i]];这个部分是我删除节点的逻辑,即跳过下一个节点,指向下下个节点。
比如有ABC三个连续节点,A–>B–>C,执行ne[i] = ne[ne[i]];就跳过了B节点。
但是这似乎是涉及三个节点的操作。当所有节点只剩下两个的时候,逻辑上会不会出错呢?实际上不会出错,当然这靠空想是很难想出来的,画图一画便知。当剩下一个节点的时候会出错吗?不会出现访问无效节点等错误,但是这最后一个节点删不掉,这便陷入了死循环。所以这时候就要考虑如何解决这个bug了。很简单,加一个if判断即可。

if(ne[i] == ne[ne[i]]) //最后一个节点的后继指针仍然是自己 
{
	cout << i << " "; 
	break;			
}

第二种思路:

#include <iostream>
using namespace std;

const int N = 110;

int n, m;

int ne[N];

int main()
{
	cin >> n >> m;
	for(int i=1;i<=n;i++)
	{
		ne[i] = i+1;
	}
	ne[n] = 1;
	
	int t = n;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<m;j++)
		{
			t = ne[t];
		}
		cout << ne[t] << " ";
		ne[t] = ne[ne[t]];
	}
	return 0;
}

为什么我会选择for(int i=1;i<=n;i++)作为循环条件,因为题目要求会将所有的编号全部出圈,现在有n个数字,我既然已经知道这n个数字会全部出圈,那么我在最外层套上n次循环就可以了。

t是遍历整个链表开始的指针,有人问是不是t = n也可以,t = 1也可以,只不过t = 1的时候循环条件改成for(int j=1;j<m-1;j++)就行了,这是错误的。首先我们要明确为什么t = n,因为我们要保证t前进m-1次就要停下,cout下一个节点的编号之后要删除下一个节点,所以循环条件必须是for(int j=1;j<m;j++)(这个条件保证了t在每次外层循环内可以前进两次),所以倒着推算,t必须从n开始才行,从1开始就走过了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值