/*************************************************************************************\
命题:数组array,长度为n,要求向右循环移动k位(0<k < n),时间复杂度和空间复杂度尽可能小。
\*************************************************************************************/
一、第一反应
对于初次遇到此命题的人,一般都会这么做:先设置一个零时变量,将数组最后一个存入变量,然后将数组中前n-1个,从最后一个开始,依次存入后面一个位置,这样做的效果就把前n-1个整体向右平移一个单位,再把零时变量的值存入数组头,如此数组完成了向右循环移动1位的工作,再外加一个循环控制,循环右移k次,于是便完成了。代码如下:
template<typename T>
void cycle_move(T* array,int n,int k)
{
for(int i = 0;i < k;i++) //k次循环右移
{
T temp = array[n-1]; //设置零时变量
for(int j = n - 1;j > 0 ;j--)
array[j] = array[j - 1];
array[0] = temp;
}
}
这是最直观的解法,算法外循环k次,内循环n-1次,k (k=1,2…n-1)是个随机值,等概率分布,循环次数的数学期望=[1*1/(n-1)+2*/(n-1)+…+(n-1)*1/(n-1)]*(n-1)=n(n-1)/2,所以时间复杂度为O(n2),空间复杂度为O(1)。时间复杂度难以让人接受。
二、换位思考
前一种方法目的是将后面的k个数据一个个的移到前面,可以换过来想想:将前面的一次性放到所要放的地方。
如:0 1 2 3 4 5 6 7右移3位
将0移到第三位,1移到第1+3位……如此原来的数需要保存,于是可以很容易想到:将0和3交换,但3也需要移到3+3位,于是可以交换此时的第0位和第6位,于是交换后第0位是6,而此时的6+3位则需要取8的模(6+3)%8=1,于是跟第1位交换……
于是大概的雏形便出现了:每次用第一位跟第(i+k)% n位的数据交换,终止条件是模为0。
伪代码如下:
/*******************************************\
1. i = 0
2. i = (i + k)% n
3. if(i == 0) 退出循环
4. swap(array[0],array[i]),转到第步
\********************************************/
貌似很简单,但是问题便出来了,(i+k)% n能访问到数组中所有的位置吗?如果能,那么每个位置是否只被访问了一次?下面将继续探讨。
三、深入探讨
首先举几个例子。
例一:0 1 2 3 4 5 6 7右移3位(n=8,k=3)
第1次 |
3 |
1 |
2 |
0 |
4 |
5 |
6 |
7 |
第2次 |
6 |
1 |
2 |
0 |
4 |
5 |
3 |
7 |
第3次 |
1 |
6 |
2 |
0 |
4 |
5 |
3 |
7 |
第4次 |
4 |
6 |
2 |
0 |
1 |
5 |
3 |
7 |
第5次 |
7 |
6 |
2 |
0 |
1 |
5 |
3 |
4 |
第6次 |
2 |
6 |
7 |
0 |
1 |
5 |
3 |
4 |
第7次 |
5 |
6 |
7 |
0 |
1 |
2 |
3 |
4 |
循环了7次,i访问了所有点,且仅仅访问了一次。
例二:0 1 2 3 4 5 6 7 8右移3位(n=9,k=3)
第1次 |
3 |
1 |
2 |
0 |
4 |
5 |
6 |
7 |
8 |
第2次 |
6 |
1 |
2 |
0 |
4 |
5 |
3 |
7 |
8 |
第3次时i = (i+k)%n = 0;于是出故障了。
例三:0 1 2 3 4 5 6 7 9右移6位(n=9,k=6)
第1次 |
6 |
1 |
2 |
3 |
4 |
5 |
0 |
7 |
8 |
第2次 |
3 |
1 |
2 |
6 |
4 |
5 |
3 |
7 |
8 |
第3次时i = (i+k)%n = 0;于是也出故障了。
再细想一下也很容易发现,当n是k的整数倍时,循环(n/k – 1)次一定会回到第0位,这是很显然的。而且条件可以更宽些,当n跟k不是互质时,循环[ n/(n,k) – 1 ]次也会回到第0位( (n,k)表示n和k的最大公约数)。
于是我们先看看(n,k) = 1时(即互质)。
1. (n,k) = 1时的情况
/************************************************\
已知(n,k) = 1
i = 0;
i = (i + k)%n;
当i == 0 时退出
求证:i可以访问数组中的每一个元素,且仅访问一次
\************************************************/
这个证明很简单:
<1>我们可以先取j = 0; j = j + k;跟i同时循环,则I = (I + k)%n 跟 j%n 是等价的。当I = (I + k)%n = 0时,则有j%n = 0,而j = j + k = pk (j初值为0,p为一正整数),于是pk%n = 0,即 pk是n的倍数,而k跟n是互质的,所以p是n的倍数,又因为只要pk%n = 0,则退出,所以p是n最小的倍数,所以必有p = n。所以i共访问了n个元素。
<2>因为I = j%n,设不同时刻的i1,i2,i1 – i2 = (j1 – j2)%n = (p1*k – p2*k)%n = (p1 – p2)*k%n,若i1 = i2,则p1 – p2一定是n的倍数,显然不同时刻p1 != p2(因为j是递增的),所以任意时刻i1 != i2。即i对n产生的模互不相等。
<3>综合1、2,得i对n的所有余数一一映射,则必然可以得到i可以访问数组中的每一个元素,且仅访问一次。
于是对于(k,n) = 1的情况可以写出代码:
template<typename T>
void cycle_move(T* array,int n,int k)
{
int i = 0;
while((i = (i + k) % n) != 0)
swap(array[0],array[i]);
}
2.考虑(k,n) != 1的情况
这种情况,有一个及其简单的思路----分段。
设(k,n) = m,则可以把n个数看成每段长度为m的n/m段数据的集合。例如:
0 1 2 3 4 5 6 7 8右移6位(n=9,k=6,m=3)
可以看成:
{0 1 2} {3 4 5} {6 7 8}三段,
此时向右移动6位即将这三段向右移动2 段(2 = k / m),鉴于前面的程序,则可以将每段中同一个位置的元素看成可以移动的集合,如{0 3 6},此时的n’跟k’必然是互质的,于是可以内嵌上面的一段程序。于是可以这样组织代码:
int greatest_common(int m,int n) //求出最大公约数
{
int r;
while(r = m % n) { m = n; n = r; }
return n;
}
template<typename T>
void cycle_move(T* array,int n,int k)
{
int r = greatest_common(n,k);
for(int i = 0;i < r;i++)
{
int j = i;
while((j = (j + k) % n) != i)
swap(array[0],array[j]);
}
}
四、多一点思考
前面所做的一切都是为了这段代码,而这段代码却如此简练。
复杂度分析:
求最大公约数时间复杂度为O(logn), cycle_move循环次数为n,而logn < n
所以该算法时间复杂度为O(n)。空间复杂度为O(1),产生在交换中。
事情到这里似乎已经很完美了,但何不多想一点?
这个程序最不好的地方在哪里呢?
很显然,最不好的地方在交换!每循环一次就得交换一次,二交换的实现类似于:
template<typename T>
void swap(T& left,T& right)
{
T temp = left;
left = right;
right = temp;
}
需要频繁地使用数组下表和这三个赋值语句,这就是我们需要改善的。
那么如何避免交换呢?
下面仍然先从(k,n) = 1说起。
前一种方法中,所有数据通过第0位作中转,以存储到目标位置,这就是交换产生的原因。如果另设一个中转的变量temp,源数据保存在temp中,每次需要将temp中的数据存到目标位,但此时目标位的数据就需要保存下来,如果保存下来了,则以此类推,得到最后一个被访问的(也就是第0位)需要被保存。这样一来就豁然开朗了:只要先保存第0位,然后按照跟前面 i 访问次序相反的次序访问,就可以不被覆盖。于是又得到下面的代码:
template<typename T>
void cycle_move(T* array,int n,int k)
{
T temp = array[0];
int i = 0,prei;
while((prei = (n + i - k) % n) != 0)
{
array[i] = array[prei];
i = prei;
}
array[i] = temp;
}
于是对于(k,n)!=1的情况可以这样组织代码:
template<typename T>
//这个是递归的写法 <br />
//author:tengzhao201 QQ:715572192<br />
//time:2010-4-24<br />
//时间复杂度为O(n),空间复杂度O(1),交换点在中间时比逆序法快一倍!!!<br />
void TZshift1(int a[],int N,int K)<br />
{<br />
K=K%N;<br />
<br />
if(0==K)return;<br />
<br />
int temp,qq,pp=0;<br />
pp=0;qq=K;<br />
for(int i=0;i<N-K;i++,pp++,qq++)<br />
{<br />
//swap(a[i%K],a[i+K]);问题的关键在于找到原来的第i个元素现在在哪里,通过观察可以发现在a[i%K]的位置,下面的代码实现了a[i%K]和a[i+K]的互换<br />
if(K==pp)pp=0;<br />
temp=a[pp];<br />
a[pp]=a[qq];<br />
a[qq]=temp;<br />
}<br />
<br />
TZshift1(a,K,K-pp);<br />
}<br />
<br />
//最高效的循环右移算法!!<br />
//非递归的写法<br />
//author:tengzhao201 QQ:715572192<br />
//time:2010-4-24<br />
//时间复杂度为O(n),空间复杂度O(1),交换点在中间时比逆序法快一倍!!!<br />
void TZshift0(int a[],int N,int K)<br />
{<br />
K=K%N;<br />
if(0==K)<br />
return;<br />
<br />
//int count=0;<br />
int temp,qq,pp=0;<br />
<br />
while(K>0)<br />
{<br />
pp=0;qq=K;<br />
for(int i=0;i<N-K;i++,pp++,qq++)<br />
{<br />
//swap(a[i%K],a[i+K]);问题的关键在于找到原来的第i个元素现在在哪里,通过观察可以发现在a[i%K]的位置,下面的代码实现了a[i%K]和a[i+K]的互换<br />
<br />
if(K==pp)pp=0;<br />
temp=a[pp];<br />
a[pp]=a[qq];<br />
a[qq]=temp;<br />
//count+=2;<br />
}<br />
N=K;<br />
if(0==pp)<br />
return;<br />
K-=pp;<br />
//TZshift(a,K,K-pp);<br />
}<br />
//cout<<"In tatal,has used "<<count<<" steps."<<endl;<br />
}
#include <iostream><br />
<br />
using namespace std;<br />
<br />
//最高效的循环右移算法!!<br />
//这个是递归的写法<br />
//author:tengzhao201 QQ:715572192<br />
//time:2010-4-24<br />
//时间复杂度为O(n),空间复杂度O(1),交换点在中间时比逆序法快一倍!!!<br />
void TZshift1(int a[],int N,int K)<br />
{<br />
K=K%N;<br />
<br />
if(0==K)return;<br />
<br />
int temp,qq,pp=0;<br />
pp=0;qq=K;<br />
for(int i=0;i<N-K;i++,pp++,qq++)<br />
{<br />
//swap(a[i%K],a[i+K]);问题的关键在于找到原来的第i个元素现在在哪里,通过观察可以发现在a[i%K]的位置,下面的代码实现了 a[i%K]和a[i+K]的互换<br />
if(K==pp)pp=0;<br />
temp=a[pp];<br />
a[pp]=a[qq];<br />
a[qq]=temp;<br />
}<br />
<br />
TZshift1(a,K,K-pp);<br />
}<br />
<br />
//最高效的循环右移算法!!<br />
//非递归的写法<br />
//author:tengzhao201 QQ:715572192<br />
//time:2010-4-24<br />
//时间复杂度为O(n),空间复杂度O(1),交换点在中间时比逆序法快一倍!!!<br />
void TZshift0(int a[],int N,int K)<br />
{<br />
K=K%N;<br />
if(0==K)<br />
return;<br />
<br />
//int count=0;<br />
int temp,qq,pp=0;<br />
<br />
while(K>0)<br />
{<br />
pp=0;qq=K;<br />
for(int i=0;i<N-K;i++,pp++,qq++)<br />
{<br />
//swap(a[i%K],a[i+K]);问题的关键在于找到原来的第i个元素现在在哪里,通过观察可以发现在a[i%K]的位置,下面的代码实现了 a[i%K]和a[i+K]的互换<br />
<br />
if(K==pp)<br />
pp=0;<br />
temp=a[pp];<br />
a[pp]=a[qq];<br />
a[qq]=temp;<br />
//count+=2;<br />
}<br />
N=K;<br />
if(0==pp)<br />
return;<br />
K-=pp;<br />
//TZshift(a,K,K-pp);<br />
}<br />
//cout<<"In tatal,has used "<<count<<" steps."<<endl;<br />
}<br />
<br />
int greatest_common(int m,int n) //求出最大公约数<br />
{<br />
while(n) {<br />
m %= n;<br />
if(m == 0) {<br />
return n;<br />
}<br />
n %= m;<br />
}<br />
return m;<br />
}<br />
<br />
template<typename T><br />
void cycle_move(T* array,int n,int k)<br />
{<br />
k %= n;<br />
int r = greatest_common(k,n);<br />
for(int i = 0;i < r;i++)<br />
{<br />
int t = i + k;<br />
int prej = i;<br />
int j = t;<br />
T tmp = array[j];<br />
do {<br />
array[j] = array[prej];<br />
j = prej;<br />
prej -= k;<br />
if(prej < 0) {<br />
prej += n;<br />
}<br />
}while(prej != t);<br />
array[j] = tmp;<br />
}<br />
}<br />
<br />
#include "MsClock.h"<br />
<br />
int main() {<br />
const int N = 100;<br />
const int K = 55;<br />
const int R = 100000;<br />
int a[N];<br />
<br />
for(int i = 0;i < N;i++) {<br />
a<i> = i;<br />
}<br />
<br />
MsClock clk;<br />
for(int i = 0;i < R;i++) {<br />
TZshift0(a,N,K);<br />
TZshift0(a,N,N-K);<br />
}<br />
cout<<clk.Now()<<endl;<br />
copy(a,a+N,ostream_iterator<int>(cout," "));<br />
cout<<endl;<br />
<br />
clk.Restart();<br />
for(int i = 0;i < R;i++) {<br />
cycle_move(a,N,K);<br />
cycle_move(a,N,N-K);<br />
}<br />
cout<<clk.Now()<<endl;<br />
copy(a,a+N,ostream_iterator<int>(cout," "));<br />
cout<<endl;<br />
<br />
return 0;<br />
}
// MsClock.h<br />
//<br />
<br />
#ifndef _MS_CLOCK<br />
#define _MS_CLOCK<br />
<br />
#include <Windows.h><br />
<br />
typedef unsigned long u_long;<br />
<br />
class MsClock {<br />
public:<br />
MsClock() {<br />
ulStart = ::GetTickCount();<br />
}<br />
<br />
void Restart() {<br />
ulStart = ::GetTickCount();<br />
}<br />
<br />
u_long Now() {<br />
return ::GetTickCount() - ulStart;<br />
}<br />
<br />
public:<br />
static u_long NowAbsolute() {<br />
return ::GetTickCount();<br />
}<br />
<br />
private:<br />
u_long ulStart;<br />
};<br />
<br />
#endif