【有趣的面試演算法題】 數組迴圈移動演算法細究

來源:互聯網
上載者:User

設計一個演算法,把一個含有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,然後再右移就好?



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.