数组循环移位,你能想到多少种算法?

设计一个算法,把一个含有 N 个元素的数组循环右移K 位

例如: abcd12345循环右移5位,得到12345abcd

第一次遇到这个问题,还是一个朋友当成考研题来问我的,并给出了进一步的限制:时间复杂度为O(N),且只允许使用两个附加变量。

后来在《编程珠玑》、《编程之美》上都看到了这道题

想来按照那个限制的难度,肯定是不会出现在考研卷上的吧,不适合做考题,所以就先无视这个限制

 

抛开各种算法不谈,就题目本身,也可以得到一些优化信息:
1.虽然题目是右移,但是左移的话也可以一样完成(数组长度为N,右移K位相当于左移N-K位)
在这种情况下,可以分别写出左移和右移的算法,然后根据K与N-K的大小关系,调用移动较少的那个
2.在K大于N的情况下,可以将右移K位简化为右移K%N位

 

自己当初也想出了多种烂算法,就当抛砖引玉啦,后面会附上几个大牛给出的做法
共有3个自己想的烂算法,3个大牛算法

==============================================================

烂算法1:

是自己最先想到的算法,时间复杂度O(N),空间复杂度O(1)

实际上是普通排序算法的变异,以两数交换为主,只不过临时变量做了小改动

还是以abcd12345右移5位为例:

a右移5位,跑到2的位置,同时临时变量记录2,此时原串变为abcd1a345

再从2的位置右移5位(位置取模),让2跑到b的位置,同时临时变量记录b,此时原串变为a2cd1a345

再从b的位置右移5位,让b跑到3的位置,同时临时变量记录3……

如此循环数组长度N次之后即可,有个缺点是取模运算比较多

==============================================================

烂算法2:

想要避免取模运算,该怎么办呢?

干脆直接把数组叠两遍,变成abcd12345abcd12345,然后从第5个位置开始截取一段连续的就行了!

其实最后一个元素不用叠,到abcd12345abcd1234就足够了

需要三次memcpy,空间复杂度比其他算法高,需要新开辟空间

==============================================================

烂算法3:

应该是最原始的算法,每次将原数组循环右移一次

temp=a[0];

for (i=0;i<length-1;++i)

    a[i]=a[i+1];

a[length-1]=temp;

移动距离为K就这么做K次
时间复杂度O(K*N)

===================笨蛋和牛人的分界线=========================

牛人提供的算法4:

如果有频繁的移位操作需求的话,干脆做一个封装用来获取元素,而不是直接取下标

这样的话所有的访问都从封装走,每次位置取个余就行了,直接一步到位了
时间复杂度O(1),空间复杂度O(1)

==============================================================

牛人提供的算法5:(个人认为是最佳算法)

对于abcd12345,将其根据位移长度(5)分为两部分,第一部分为第1个元素至第N-K个元素:

abcd和12345

然后先将这两个部分内部分别倒序,得到dcba 54321

然后再将整个数组倒序,就能得到结果12345abcd

这个大概是符合那位同学所说的“时间复杂度为O(N),且只允许使用两个附加变量”变态要求的答案了。

其原理为UV=((V-1) (U-1))-1(-1为上标,表示取逆)

真是太巧妙了。

 

顺便说一下,STL里面的rotate函数<algorithm>,就是用这种方法实现的

template<class _BidIt> inline
void _Rotate(_BidIt _First, _BidIt _Mid, _BidIt _Last,   bidirectional_iterator_tag)
{
    std::reverse(_First, _Mid);
    std::reverse(_Mid, _Last);
    std::reverse(_First, _Last);
}

===============================================================

牛人提供的算法6:(很长很长,涉及数学理论,只好一股脑转载了。这也是一个时间复杂度O(N)的算法)
(将此算法与烂算法1进行对比,可深刻体会笨蛋与牛人的区别)
循环置换分解定理:对于给定数组A[0..N-1]向后循环换位N-K位运算,可分解为恰好gcd(K,N-K)个循环置换,且0,...,gcd(K,N-K)-1中的每个数恰属于一个循环置换。其中gcd(x,y)表示x和y的最大公因数。

我们从头开始分析这个问题,对于数组A[0..n-1],要将其向后循环移动k位元素。因为每个元素右移n位后又回到了原来的位置上,所以右移k位等于右移k mod n位。考虑每个元素右移k位后的最终位置,比如对于A[0],右移k位后在k mod n位置上,原来在k mod n位置上的元素右移k位后到了2*k mod n的位置上,把如此因为A[0]的移动而受到连环影响必须移动的位置列出来,就是下面这样一个位置序列:0,k,2*k,...,(t-1)k。其中每一项都是在模n的意义下的位置。t*k mod n 的结果是0。t是使得t*k mod n的结果为0的最小正整数。

这个位置序列实质上是模n加法群中由元素k生成的一个循环子群。由群论中的结论(该结论的证明见最后)知,循环子群(k)的周期为n / gcd(k,n),元数为n / gcd(k,n),其陪集个数为gcd(k,n)。换句话说,A[0..n-1]循环右移k位的结果是循环子群(k)以及它的所有陪集循环右移一位。例如,将A[0..5] = {1,2,3,4,5,6}循环右移4位,这里n = 6, k = 4, gcd(k, n) = 2。A[0]的最终位置是4,A[4]的最终位置是2,A[2]的最终位置是0,这样,位置0,4,2便是由k=4生成的循环群,周期为6 / gcd(4,6) = 6 / 2 = 3,这样的循环子群共有gcd(4,6) = 2个。

完整代码如下:
// 数组的循环移位

#include <cstdio>

int gcd(int m, int n) {

    int r;

    while(r = m % n) {

        m = n; n = r;

    }

    return n;
}

void shiftArray(int A[], int n, int k) {

    // 因为左移的代码比右移的代码好实现的多,而右移k位

    // 等价于左移-k位,-k = n - k。以下代码是通过左移-k位来实现右移k位

    k = n - (k % n);

    for(int i = 0, cnt = gcd(n, k); i < cnt; i++) {

        int t = A[i], p = i, j = (k+i) % n;

        while(j != i) {

            A[p] = A[j]; p = j; j = (k + p) % n;

        }

        A[p] = t;

    }

}

void printArray(int A[], int n) {
    for(int i = 0; i < n; i++) {

        printf("%-3d", A[i]);

        if((i+1)%10 == 0) printf("/n");

    }

}

int main() {

    int A[] = {1,2,3,4,5,6, 7};

    shiftArray(A, 7, 4);

    printArray(A, 7);

    return 0;

}

上述所用到的那个群论结论的证明:

结论:设G是一个循环群,其中一个生成元素为a,若r和n的最大公约数为d,则a^r的周期为n / d。

在模n加法群中,1是一个生成元素,任意元素k=1*k,所以任意元素k生成的循环子群(k)的周期为n / gcd(k,n)。因为gcd(k,n)=gcd(k,n-k),所以也可以写成n / gcd(k, n-k)。

 

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>