问题描述:约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。(摘自百度百科)
问题分析:约瑟夫环是一个数学问题,但是其实并不需要急于列出列出约瑟夫环的数学解法(约瑟夫环的数学解法时间复杂度最好,空间复杂度也最低,后续文章将有介绍)。为了能让计算机解决这个数学问题,我们要做的是让计算机去模拟实现这一个过程。约瑟夫环整个过程其实重复做一件事,是从当前位置寻找m个位置之后的人,然后将这个人踢出去。因为总的人数是有限的,那么必然是数到尾部又从新回到开头。这篇文章使用双链表实现这个过程。
为了能够实现约瑟夫环,我们要进行更深入分析这个过程,从而确定我们要在双链表上附加哪些功能。
上图是一个双向链表,双向链表的特点是每个节点都拥有对前一个和后一个节点的地址,也就是一个prev和一个next
typedef struct Node
{
ElementType Data;
struct Node *Prev;
struct Node *Next;
}Node;
typedef Node *DoubleLinkedList;
typedef Node *PtrToNode;
这个结构将是这篇文章用的数据结构。
接下来,我们回到约瑟夫环问题,我们要考虑一些函数,这些函数的功能加到双链表以后,我们能完成约瑟夫环的功能。
首先,我们需要一个DeleteIth()函数,这个函数的功能是删除第i个元素。 为什么需要这个函数? 因为我们的约瑟夫环每一个循环都是要删除一个元素的,而且约瑟夫环的进行过程是只针对序列号的,与数据区域并没有关系,所以我们需要一个能删除第i个元素的函数,而不需要一个根据数据域来删除元素的函数
void DeleteIth(DoubleLinkedList L,int i);
接下来,我们继续分析一下,为了要删除第i个元素,那么你是否就需要第i个元素的地址? 所以,我们需要LocateIth()函数用来获得第i个元素的地址
PtrToNode LocateIth(DoubleLinkedList L,int i);
最后,我们需要的另一个函数是约瑟夫环的灵魂问题。约瑟夫环问题说简单一点就是不断地删除节点,每一次过程删除一个节点,直到所有人都被删除为止。那么这个过程是个
什么过程?
抛弃一些多余的东西后,约瑟夫环每次删除的过程是从当前位置寻找之后m个人的过程。这个过程麻烦在,我们一共就那么多人,从你开始的人数起,这之后的人都数光
了,还没有到m,怎么办?哈,其实问题很简单,每次你数到尾巴了,我就让你回到列表第一个人开始继续计数不就行了? 这个过程可以通过countNext()实现
int CountNext(DoubleLinkedList L,int k,int m);
把链表传进去的原因是用来确定链表的长度的,k表示从第k个人数起,m表示数m个人
实例解析:
假设,现在我们有七个人,他们的数据分别是 3 1 7 24 8 4 ,我们从第2个人开始数起,每次数3个人
第一次:从第2个人开始数3个人,我们调用CountNext(L,2,3)来实现,它应该返回4,删除链表中的第四个元素,也就是2。因为CountNext返回的是要删除的元素的序列号,
所以我们可以通过调用DeleteIth(L,4)来删除这个元素,删除之后的列表变为 3 1 7 48 4
第二次:删除2以后,我们接下来要从2的下一个元素4开始往后面数3个人来实现,也就最后一个数4,通过调用CountNext(L,4,3)应该返回7,然后我们通过调用DeleteIth(L,7)来
删除最后一个元素,列表变为 3 1 7 24 8
第三次:第二次删除的4我们已经到最后一个元素了,为了能继续数,我们应该为CountNext里面的k设置为1了,这时候对列表调用CountNext(L,1,4),然后删除对应元素
....
第七次:......
再次分析约瑟夫过程:
通过上面的实例分析,我们不难发现,在每次这样的重复中,一直在更改的便是CountNext(L,k,m)中的k这个参数,我把它看做每次循环的初始位置,叫做intialLocation,
每次循环过程中都要更新这个intialLocation()从而能正确寻找到下一个要删除的元素。
<pre name="code" class="cpp">代码实现(部分):
<pre name="code" class="cpp">int CountNext(DoubleLinkedList L,int k,int m)
{
int Length=ListLength(L);
int count=1; //计数,计算当前数过了多少个元素
int i,j;
i=k;
while(count<m)
{
if(i+1<=Length)
{
i++;
count++;
}
else if(i+1>Length) //当访问超过了数组长度,我们要让他回到1
{
i=1;
count++;
}
}
return i;
}
</pre><pre name="code" class="cpp">约瑟夫过程
<pre name="code" class="cpp"> while(!Empty(L)) //只要元素没有删除完,就一直持续约瑟夫过程
{
<span> </span>int Length=ListLength(L);
nextLocation=CountNext(L,initLocation,m);//获得要删除元素的索引
DeleteIth(L,nextLocation); //删除这个元素
if(nextLocation==Length) //如果这个元素是最后一个元素,那么他的下一个元素应该是链表的第一个元素
{
initLocation=1;
}
else
{
initLocation=nextLocation;//如果删除的元素不是最后一个元素,下一轮循环的初始位置设置为删除的位置
<span> </span>//!!!因为链表的长度减少1了,所以初始位置并不需要要加1!!!
}
}
总结:
这篇文章是通过双线链表来实现的,时间复杂度到达O(mn) ,当问题范围很大的时候根本不可取,空间复杂度为O(n),当n很大的时候,同样不可取。
接下来的文章会通过循环链表和数组以及数学方法实现。本人更推崇数学解法,因为数学解法的时间复杂度是O(n),另外几个都是O(mn),想比较起来速度快太多,而空间复杂度和另外几个相差并不大