今天是释然发题解的第六天,以后每一天都会和大家分享学习路上的心得,希望和大家一起进步,一起享受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
本文深入探讨约瑟夫问题的三种解法:数组、环链表及递归,对比不同算法的时间复杂度,揭示递归法如何将时间复杂度降至O(n)。

被折叠的 条评论
为什么被折叠?



