设计一个算法,把一个含有N个元素的数组循环右移K位,要求:时间复杂度为O(N),且只允许使用两个附加变量。
最直接的想法是“一步到位”,尽量避免数据移动或者交换,于是有像下面的这样的代码:
template<typename T>
void shiftArrRightCir(T* arr, const int N, int k)
{
k %=N;
const int divN =N/k;
for (int i = 0; i < k; i++)
{
T tmp=arr[i];
for (int j = 0; j < divN; j ++)
{
arr[(N +i -j*k)%N]= arr[(N +i -j*k -k)%N];
}
arr[(i +k)%N] =tmp;
}
}
调试了下之后发现一个问题,只有 k 与 n 刚好整除时才运行正常,否则会有 n%k 个数据没有更新或者丢失,于是再上网,看到有高人是这样处理的: http://www.dewen.org/q/6262
template<typename T>
void shiftArrRightCir(T* arr, const int n, int k)
{
k %= n;
//求出N和k的最大公约数(欧几里得辗转相除法)
int g1,g2;
g1 = n;
g2 = k;
while(g2 != 0)
{
g1 = g1 % g2;
g1 = g1 ^ g2;
g2 = g1 ^ g2;
g1 = g1 ^ g2;
}
//复用变量g1,g2做为循环变量
for(g1--;g1>=0;g1--)
{
for(g2 = g1; (g2 + n - k) % n != g1; g2 = (g2 + n - k) % n)
{
arr[g2] = arr[g2] ^ arr[(g2 + n - k) % n];
arr[(g2 + n - k) % n] = arr[g2] ^ arr[(g2 + n - k) % n];
arr[g2] = arr[g2] ^ arr[(g2 + n - k) % n];
}
}
}
原理:循环移位时,每个元素m移到(m+k) % n的位置上,而(m+k)%n移到(m+2k)%n的位置上……
当n与k互质时,可以证明m, m+k, m+2k, ..., m + (n-1)k模n的结果互不相等,刚好构成一个循环;否则,会构成(n,k)(n与k的最大公约数)个循环。分别处理这(n,k)个循环就可以得到结果。
对应地,给上面的一段代码稍做改动,并加上注释,是不是要好理解多了呢?
template<typename T>
void shiftArrRightCir(T* arr, const int n, int k)
{
k %= n;
//求出N和k的最大公约数(欧几里得辗转相除法)
int g1 = n;
int g2 = k;
while(g2 != 0) //余数为零时中止算法,g1就是最大公约数
{
g1 = g1 % g2; //较大的数对较小的数取余
g1 = g1 ^ g2; //异或法交换 g1 g2的值
g2 = g1 ^ g2;
g1 = g1 ^ g2;
}
//复用变量g1,g2做为循环变量
for(--g1; g1>=0; --g1)
{
g2 =g1;
int z =(g2 + n - k) % n;
while(z != g1) //是否达到这一轮的起点
{
//异或法交换 z g2 所指的值
arr[g2] = arr[g2] ^ arr[z];
arr[z] = arr[g2] ^ arr[z];
arr[g2] = arr[g2] ^ arr[z];
//修正下标值,指向下一次 要交换的数据对
g2 = (g2 + n - k) % n;
z = (g2 + n - k) % n;
}
}
}
到了这里,不禁想,这种思路能否与前面的“一步到位”想法相结合呢? 省去多次交换,效率岂不更是高效?
template<typename T>
void shiftArrRightCir(T* arr, const int n, int k)
{
k %= n;
//求出N和k的最大公约数(欧几里得辗转相除法)
int g1 = n;
int g2 = k;
while(g2 != 0) //余数为零时中止算法,g1就是最大公约数
{
g1 = g1 % g2; //较大的数对较小的数取余
g1 = g1 ^ g2; //异或法交换 g1 g2的值
g2 = g1 ^ g2;
g1 = g1 ^ g2;
}
//复用变量g1,g2做为循环变量
for(--g1; g1>=0; --g1)
{
T tmp=arr[g1]; //暂时保存起点值
g2 =g1; //g2 此时起总是指向数据 接收/移入 方下标
int g3 =(g2 + n - k) % n; // g3 总是指向下一个要 移出 的数据下标
while(g3 != g1) //是否达到这一轮的起点
{
arr[g2] = arr[g3];
//修正下标值,指向下一次 要移动的数据
g2 = g3;
g3 = (g3 + n - k) % n;
}
arr[g2]=tmp; //移动起点
}
}
调试了一下,基本通过,爽!!!
当然也有人使用三次逆序方法达到了目的,应该是相对容易理解的高效方法:
Reverse(int *arr, int b, int e) //逆序排列
{
for( ; b < e; b++, e--) //从数组的前、后一起遍历
{
int temp = arr[e];
arr[e] = arr[b];
arr[b] = temp;
}
}
RightShift(int *arr, int N, int K)
{
K = K % N ;
Reverse(arr, 0, N-K-1); //前面N-K部分逆序
Reverse(arr, N-K, N-1); //后面K部分逆序
Reverse(arr, 0, N-1); //全部逆序
}
还有其它使用递归分治什么思路的,代码比较冗长,没兴趣关注,呵呵
一个问题,上述方法,如果要改造为左移,适应性强么?
直接在 k %=n, 之后增加 k =n -k,然后再右移就好?