深度剖析Josephus ring(约瑟夫环)C语言版
鉴于C语言更适合展示算法的底层设计,并且便于读者的研究与思考,故而小编使用C语言来展示约瑟夫环的精巧与奥妙。
Hello!!各位同学们,欢迎来到白哥的小学二年级课堂,想想好久没有更新了,那么这次白哥跟大家来聊聊一个非常有趣的问题——约瑟夫环。
前言
问题说明
N个人围成一圈(从1到N按顺序排好),从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=10,M=3,被杀掉的顺序是: 3 ,6 ,9 ,2 ,7 ,1 ,8 ,5 ,10 ,4。
这里我们用数组的方式先来解决这个问题
先是一个完整的数组
我们用k来记录向前报数的情况,并且i从0开始跟随着报数向前滚动,当k报到3时当前位置的数字删除,数组的个数减1,接先来因为删去的位置由下一个数来填补,所以i不用自加,并且在当前位置进行新一轮的报数,当k向前报数时,i 要跟着k向前自加,如图:
向后依次类推,最终在i=6时进行第4轮报数,当k报第2个数时我们发现 i++已经溢出当前数组,所以我们要想办法让 i 回到数组首位,故而我们给 i 进行取余操作,i=(i+1)%N,此时的N是当前数组的个数,这也就是为什么我们在每删除一个数后,要对N进行减一的操作。
由此这样依次循环下去,当N=1时停止循环,因为此时数组中只有一个元素,故而我们将剩下的那个数在循环外做一步删除操作即可(在循环内跑一遍亦可以,但要将跳出循环的条件改为(N>=0))。
下面我们来看一个最初始的代码,然后我们再来做进一步的优化。
void JosephusRing(int a[], int n, int m)
{
int i = 0, j, k;
while (n > 0)
{
for (k = 1; k < m ; i = (i + 1) % n, ++k); //将k作为指针,和i一起向前滚动,i最终滚到的位置便是要删去的位置
printf("%5d", a[i]); //将要删除的先输出,后面我们在做优化
for (j = i; j < n - 1; ++j) //数组删除操作
{
a[j] = a[j + 1];
}
n--; //注意删除后数组个数减一
}
printf("%5d", a[0]); //将数组中最后一个数弹出
}
接下来,我们再来思考,我们让 i 跟随k一起向前滚动本质上是给 i 每次加了m-1个位置然后做取余操作,那么我们就可以将上面的
for (k = 1; k < m ; i = (i + 1) % n, ++k); //将k作为指针,和i一起向前滚动,i最终滚到的位置便是要删去的位置
直接变为一步数学变换
i = (i + m - 1) % n; //可通过规律直接求得下一个要删除的数的位置,不用向前一个一个寻找
展示完整的代码方便读者阅读
void JosephusRing(int a[], int n, int m)
{
int i = 0, j;
while (n > 0)
{
i = (i + m - 1) % n; //可通过规律直接求得下一个要删除的数的位置,不用向前一个一个寻找
printf("%5d", a[i]);
for (j = i; j < n - 1; ++j) //数组删除操作
{
a[j] = a[j + 1];
}
n--; //注意删除后位数减一
}
printf("%5d", a[0]);
}
接下来,我们发现刚才的代码我们将数据删除的操作是将它输出,那如果我们要将他存储起来呢?(显然我们可以开一个数组去存)但小编这里讲的并非如此做法。
我们仔细观察我们在做删除操作的循环时的代码:
for (j = i; j < n - 1; ++j) //数组删除操作
{
a[j] = a[j + 1];
}
我们发现当删完之后我们的 j 指向了当前删除后数组有效位的最后一位,因此如果我们将每个删除的数放在这个位置将会节省另外开辟数组的空间,我们删除的顺序只不过在数组中倒着排而已,别急我们一会还要拿倒着排的特点做更好的操作,我们先看图,这样便于读者理解:
第一次删除之后的情形:
第二次删除之后的情形:
故而依次类推,我们可以将删除的数放在同于原数组同一片空间中。看代码:
void JosephusRing(int a[], int n, int m)
{
int i = 0, j,x;
while (n > 0)
{
i = (i + m - 1) % n;
x=a[i]; //可将要删的数保留
for (j = i; j < n - 1; ++j) //进行删除操作,发现当j跳出循环时,j的位置在当前数组有效位的后一位,故而可将要删除的数放在数组有效位后
{ //这样最后数组循环完后,数组将变为一个删除数据由后向先排成的数组,这样直接将数组倒序输出即为正确答案。
a[j] = a[j + 1]; //这样即将删除的先后直接存入该数组,大大的减少了空间的占用。
}
a[j] = x; //或者可以为a[n-1]=x;
n--;
}
}
总体的,当我们给出一列数从0到n循环输入后,经过JosephusRing函数从n-1到0输出便是删除数的先后顺序了。
#include<stdio.h>
void JosephusRing(int a[], int n, int m);
void JosephusRing(int a[], int n, int m)
{
int i = 0, j, x;
while (n > 0)
{
i = (i + m - 1) % n;
x = a[i]; //可将要删的数保留
for (j = i; j < n - 1; ++j) //进行删除操作,发现当j跳出循环时,j的位置在当前数组有效位的后一位,故而可将要删除的数放在数组有效位后
{ //这样最后数组循环完后,数组将变为一个删除数据由后向先排成的数组,这样直接将数组倒序输出即为正确答案。
a[j] = a[j + 1]; //这样即将删除的先后直接存入该数组,大大的减少了空间的占用。
}
a[j] = x; //或者可以为a[n-1]=x;
n--;
}
}
int main(void)
{
int a[10];
int n = 10; //可以自行去定义n,再此不做说明
int i;
for ( i= 0; i < 10; ++i)
{
a[i] = i + 1;
}
JosephusRing(a, 10, 3);
for (--i; i >= 0; --i) //借助输入时的 i 倒着输出,即使删除顺序
{
printf("%5d",a[i]);
}
return 0;
}
好了,用数组的方法我们讲完了,希望同学们可以从中获得新的技巧与思想。接下来我们用循环链表的方式直观的将这个问题进行总结,循环链表因为首尾相接所以删除起来就会方便许多。
我们直接上代码:
#include<stdio.h>
#include <stdlib.h>
typedef struct node {
int data;
struct node* next;
}ElemSN;
ElemSN* InitRing(int n);
void JosephusRing(ElemSN* t, int n, int m);
ElemSN* InitRing(int n) {
ElemSN* t = NULL, * p;
for (int i = 0; i < n; ++i)
{
p = (ElemSN*)malloc(sizeof(ElemSN));
p->data = i + 1;
if (!t)
{
t = p->next = p; //给头指针
}
else {
p->next = t->next;
t = t->next = p; //保证每加进来一个节点链表都是循环链表
}
}
return t;
}
void JosephusRing(ElemSN* t, int n, int m)
{
int i = 0;
while (n > 0)
{
i = (i + 1) % m;
if (i)
{
t = t->next;
}
else {
ElemSN* p = t->next;
t->next = p->next;
printf("%d", p->data);
free(p);
n--;
}
}
return;
}
int main(void) {
ElemSN* tail; //初始化节点
int n = 10, m = 3;//m与n 可进行赋值,此处不做说明
tail = InitRing(n);
//实现JosephusRing
JosephusRing(tail,n,m);
return 0;
}
很多同学在阅读的时候可能不是很明白这样一段代码,这段代码意思是我们每加进来一个节点,总要使链表形成循环链表的状态,并且我们链表的指针指向尾结点,为什么指向尾结点呢?我们一会来解释,我们先来说明这一段代码的意义:
if (!t)
{
t = p->next = p; //给尾指针
}
else {
p->next = t->next;
t = t->next = p; //保证每加进来一个节点链表都是循环链表
}
当t为空时,当加入一个元素,我们要创建循环链表,如图这样:
当t不为空时向t中加入新建的p节点,如图:
先将t的next给p的next
然后将p给t的next连接起来
然后移动t到链尾
依次做完后形成循环链表
那么我们为什么要找到尾结点呢,因为我们后面再进行JosephusRing函数的时候我们要找到要删除数的前驱节点才能删除,而从尾结点开始先后遍历刚好能够找到要删除数的尾结点,这也就是我们要找到尾结点的原因。
希望读者可以仔细阅读前面的代码,一定会有所收获。
好啦,这就是本次小编为大家带来的关于深度剖析Josephus ring(约瑟夫环)的全部内容啦!感谢您的阅读,分享知识,这个世界还能更好!我们下期再见!