【有趣的面试算法题】 数组循环移动算法细究

设计一个算法,把一个含有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,然后再右移就好?



    原文作者:智慧视觉
    原文地址: https://blog.csdn.net/zfdxx369/article/details/10050325
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞