问题描述:0,1,2......,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里最后剩下的数字。
例如:0,1,2,3,4这5个数字组成一个圆圈,从数字0开始每次删除第三个数字,则删除的前四个数字依次是2,0,4,1,因此最后剩下的数字是3.
解法一:
由题目中的圆圈和删除,可以很自然的想到用环形链表的删除节点解决问题。可以创建一个有n个节点的环形链表,然后每次删除链表中的第m个节点。
●C语言实现:
1>定义节点的结构,每个节点由数据域和指针域组成:
typedef int DataType;
typedef struct Node
{
struct Node* _pNext;
DataType data;
}Node,*PNode;
2>初始化链表,通过尾插或头插创建一个单链表:
void SListInit(PNode* pHead)
{
assert(pHead);
*pHead = NULL;
}
PNode BuySListNode(DataType data)
{
PNode pNewNode = (PNode)malloc(sizeof(Node));
if (pNewNode == NULL)
{
//申请失败
return NULL;
}
else
{
pNewNode->data = data;
pNewNode->_pNext = NULL;
}
return pNewNode;
}
void SListPushBack(PNode* pHead, DataType data)
{
PNode pCur = NULL;
PNode pNewNode = NULL;
assert(pHead);
pCur = *pHead;
pNewNode = BuySListNode(data);
if (pNewNode == NULL)
return;
//空链表
if (*pHead == NULL)
{
*pHead = pNewNode;
return;
}
//非空链表
while (pCur->_pNext)
{
pCur = pCur->_pNext;
}
pCur->_pNext = pNewNode;
}
这是采用尾插法创建的一个单链表,那么尾插好节点只需要让链表的最后一个元素指向链表的第一个节点,这样就形成了环形链表(也就是约瑟夫环)。
3>要删除第m个节点,就要先找到第m个节点然后再删除它。(先报数再删除)
void JosephCircle(PNode *pHead, const int M)
//或者返回PNode
{
PNode pCur = NULL;
assert(pHead);
pCur = *pHead;
while(pCur->_pNext!=pCur)
{ //报数
int count = M;
while (--count)
pCur = pCur->_pNext;
//删除节点
PNode pDel = pCur->_pNext;
pCur->data = pDel->data;
pCur->_pNext = pDel->_pNext;
free(pDel);
}
//pHead有可能已经被删除
*pHead = pCur;
//return pHead;
}
●C++实现:
上面是采用C语言解决的,下面我们用C++中模板库中的std:list来模拟环形链表。
由于std::list本身并不是环形结构,因此每当迭代器走到链表末尾的时候,把迭代器移到链表的头部,这样就相当于迭代器一直在环形链表中遍历了。
#include<iostream>
#include<list>
using namespace std;
int JosephCircle(int n, int m)
{
if (n < 1 || m < 1)
{
return -1;
}
int i = 0;
//创建链表
list<int> number;
for (i = 0; i < n; i++)
{
number.push_back(i);
}
list<int>::iterator lit = number.begin();
while (number.size()>1)
{
for (i = 1; i < m; ++i)
{
lit++;
if (lit == number.end())
lit = number.begin();
}
list<int>::iterator next = ++lit;//待删除节点的下一位置
if (next == number.end())
next = number.begin();
--lit;
number.erase(lit);
lit = next;
}
return *(lit);
}
int main()
{
cout<<JosephCircle(5, 3)<<endl;
system("pause");
return 0;
}
上例中我实现了一个节点为0,1,2,3,4的环形链表,每次删除第三个节点,最终剩下的节点是3。
仔细分析上述代码,就会发现,如果n比较大,代码运行时,它会在环形链表中重复遍历很多遍。这种方法每删除一次要进行m步运算,共有n个数字,因此这个代码的时间复杂度是O(mn)。而且需要一个链表模拟圆圈,空间复杂度为O(n),因此它的效率不是很高。
解法二:
找出每次被删除的数字的规律,找一种更高效的方法。
1> 定义一个关于m和n的方程f(n,m),表示每次在n个数字0,1,2,.....,n-1,中删除第m个数字最后剩下的数字。
2>n个数字中,设第一次删除的数字是k,通过分析,可以得出k=(m-1)%n,那么删除k之后剩下的n-1个数字就是0,1,2......,k-1,k+1,.....n-1,而且下一次删除的数字从k+1开始计数,相当于这n-1个数字的排列顺序是k+1,...n-1,0,1...k-1。这个序列最后剩下的函数也应该是关于m和n的函数,这个函数记为f(n-1,m)。最初序列最后剩下的数字一定是删除一个数之后的序列最后剩下的数字,即f(n,m)=f(n-1,m)。
3> 把剩下的这n-1个数字的序列映射起来,映射成一个0~n-2的序列。
4>映射定义为p,则p(x)=(x-k-1)%n。表示映射前的数为x,映射后为(x-k-1)%n。该映射的逆映射为p~(x)=(x+k+1)%n。
映射之后的序列和最开始的序列都是从0开始的连续序列,因此映射后的序列可以用f(n-1,m)来表示。
这样我们就找到了f(n,m)和f(n-1,m)的关系,不难发现,这个递归公式意思是,要找到n个数中删除第m个数字之后剩下的数,就要先找到n-1个数中删除第m个数字之后剩下的数,以此类推,直到最后剩下一个数。当只有一个数,最后剩下的数字就是0了。
也就是如下的公式:
用一个for循环就可以很简单的实现它:
int JosephCircle(int n, int m)
{
if (n < 1 || m < 1)
{
return -1;
}
int last = 0;
for (int i = 2; i <= n; i++)
{
last = (last + m) % i;
}
return last;
}
int main()
{
printf("%d\n",JosephCircle(5, 3));
system("pause");
return 0;
}
这种算法分析起来比较复杂,但是仅仅用几行的代码就可以搞定,而且,它的时间复杂度是O(n),空间复杂度是O(1),比前面的环形链表更加高效。