關於約瑟夫環的幾種求解問題
問題描述: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),比前面的環形連結串列更加高效。