Google的一個面試題——數組原地置換

來源:互聯網
上載者:User

給定一個數組a1,a2,a3,...an,b1,b2,b3..bn,最終把它置換成a1,b1,a2,b2,...an,bn。

分析:

本題是完美洗牌問題的變形。

完美洗牌問題:   給定一個數組a1,a2,a3,...an,b1,b2,b3..bn,最終把它置換成b1,a1,b2,a2,...bn,an。

我們先解決完美洗牌問題。

為方便起見,我們考慮的是一個下標從1開始的數組,下標範圍是[1..2n]。 我們看一下每個元素最終去了什麼地方。

前n個元素 a1 -> a2  a2->a4.... 第i個元素去了 第(2 * i)的位置。

後n個元素a(n + 1)->a1, a(n + 2)->a3... 第i個元素去了第 ((2 * (i - n) ) - 1) = (2 * i - (2 * n + 1)) = (2 * i) % (2 * n + 1) 個位置。

統一一下,任意的第i個元素,我們最終換到了 (2 * i) % (2 * n + 1)的位置,這個模數很神奇,不會產生0。所有最終的位置編號還是從1到2n。

(1) 完美洗牌演算法1

於是,如果允許我們再用一個數組的話,我們直接把每個元素放到該放得位置就好了。於是產生了最簡單的方法pefect_shuffle1,它的時間複雜度是O(n),空間複雜度也是O(n)。

代碼:

// 時間O(n),空間O(n) 數組下標從1開始void pefect_shuffle1(int *a,int n) {int n2 = n * 2, i, b[N];    for (i = 1; i <= n2; ++i) {        b[(i * 2) % (n2 + 1)] = a[i];    }    for (i = 1; i <= n2; ++i) {        a[i] = b[i];    }}

(2) 完美洗牌演算法2——分治的力量。

考慮分治法,假設n也是偶數。 我們考慮把數組拆成兩半:

       我們唯寫數組的下標:

原始數組(1..2*n)

也就是(1..n)(n + 1.. 2 *n)

前半段長度為n,後半段長度也為n,我們把前半段的後n / 2個元素(n / 2 + 1..n)  與  後半段的前n / 2 個元素 (n + 1..n + n / 2 ) 交換,得到 :

新的前n個元素A : (1..n / 2)(n + 1..n + n / 2 )

新的後n個元素B : (n / 2 + 1.. n) (n + n / 2 + 1..n)

因為n是偶數,我們得到了A,B兩個子問題。問題轉化為了求解n' = n / 2的兩個問題。

當n是奇數怎麼辦?我們可以把前半段多出來的那個元素先拿出來,後面所有元素前移,再把當時多出的那個元素放到末尾,這樣數列最後兩個元素已經滿足要求了。於是只考慮前2 * (n - 1)個元素就可以了,於是轉換成了(n - 1)的問題。

為了說明問題,我們還是用a, b 分別說明一下。假設n = 4是個偶數,我們要做的數列是:

a1, a2,a3,a4,b1,b2,b3,b4

我們先要把前半段的後2個元素(a3,a4)與後半段的前2個元素(b1,b2)交換,得到a1,a2,b1,b2,a3,a4,b3,b4。

於是,我們分別求解子問題A (a1,a2,b1,b2)和子問題B (a3,a4,b3,b4)就可以了。

如果n = 5,是奇數數怎麼辦?

我們原始的數組是a1,a2,a3,a4,a5,b1,b2,b3,b4,b5

我們先把a5拎出來,後面所有元素前移,再把a5放到最後,變為:

a1,a2,a3,a4,b1,b2,b3,b4,b5,a5

可見這時最後兩個元素b5,a5已經是我們要的結果了,所以我們只要考慮n=4就可以了。

那麼複雜度怎麼算? 

每次,我們交換中間的n個元素,需要O(n)的時間,n是奇數的話,我們還需要O(n)的時間先把後兩個元素調整好,但這步影響總體時間複雜度。所以,無論如何都是O(n)的時間複雜度。

於是我們有 T(n) = T(n / 2) + O(n)  這個就是跟歸併排序一樣的複雜度式子,最終複雜度解出來T(n) = O(nlogn)。空間的話,我們就在數組內部折騰的,所以是O(1)。(當然沒有考慮遞迴的棧的空間)

代碼:

//時間O(nlogn) 空間O(1) 數組下標從1開始void perfect_shuffle2(int *a,int n) {int t,i;    if (n == 1) {        t = a[1];        a[1] = a[2];        a[2] = t;        return;    }    int n2 = n * 2, n3 = n / 2;    if (n % 2 == 1) {  //奇數的處理        t = a[n];        for (i = n + 1; i <= n2; ++i) {            a[i - 1] = a[i];        }        a[n2] = t;        --n;    }    //到此n是偶數        for (i = n3 + 1; i <= n; ++i) {        t = a[i];        a[i] = a[i + n3];        a[i + n3] = t;    }        // [1.. n /2]    perfect_shuffle2(a, n3);    perfect_shuffle2(a + n, n3);    }

(3) 完美洗牌演算法。

 這個演算法源自一篇文章,文章很數學,可以只記結論就好了……

這個演算法的具體實現還是依賴於演算法1,和演算法2的。

首先,對於每一個元素,它最終都會到達一個位置,我們如果記錄每個元素應到的位置會形成圈。

為什麼會形成圈?

比如原來位置為a的元素最終到達b,而b又要達到c……,因為每個新位置和原位置都有一個元素,所以一條鏈

a->b->c->d……這樣下去的話,必然有一個元素會指向a,(因為中間那些位置b,c,d……都已經被其它元素指向了)。

這就是圈的成因。

比如 6個元素 

原始是(1,2,3,4,5,6), 最終是(4,1,5,2,6,3),我們用a->b表示原來下標為a的元素,新下標為b了。

1->2

2->4

3->6

4->1

5->3

6->5

我們會發現1->2->4->1是個圈,3->6->5->3是另外一個圈。可以表示為(1,2,4) 和(3,6,5),演算法1已經告訴我們每個位置i的元素,都變為2 * i % (2 * n + 1),那麼我們只要知道圈裡的任意一個元素,順著走一遍就可以了,圈與圈是不相交的,所以這樣下來,我們就只走了O(n)步。

我們不能從1開始對每個元素都沿著圈走一圈。這是因為例如上例中(1,2,4)這個圈,我們只能從1,2, 4中的一個開始走一圈,而不能從1走一圈,再從2走一圈,再從4走一圈……因此我們可能還需要O(n)的空間記錄哪些元素所在的圈已經處理了,但是這樣做的時空複雜度等同於演算法1了。、(如果數組元素都是正數,我們可以把處理過的元素標記為負數,最後再取相反數,但是這樣做其實也是利用了額外的空間的)。那麼我們的關鍵問題是從每個圈裡選擇任意一個位置作代表,每個圈從這個位置位置走一圈。而空間複雜度要求我們最好不要提前把每個代表位置存下來。後文一個定理表明,我們在數組長度滿足一定條件情況下,每個圈中得最小元素可以簡單地表達出來。於是,我們稱每個圈的最小的位置叫做圈的頭部,用它來作圈的代表位置。如上例中圈(1,2,4)的頭部是1,(3,6,5)的頭部是3。如果我們知道了一個圈的頭部,用它做代表,沿著這個圈走就可以了。沿著圈走得算叫做cycle_leader,這部分代碼如下:

//數組下標從1開始,from是圈的頭部,mod是要模數的數 mod 應該為 2 * n + 1,時間複雜度O(圈長)void cycle_leader(int *a,int from, int mod) {int last = a[from],t,i;        for (i = from * 2 % mod;i != from; i = i * 2 % mod) {        t = a[i];        a[i] = last;        last = t;            }    a[from] = last;}

那麼如何找到每個圈的頭部呢?引用一篇論文,名字叫:

A
Simple In-Place Algorithm for In-Shuffle. 

Peiyush Jain, Microsoft Corporation. 

利用數論知識,包括原根、甚至群論什麼的,論文給出了一個出色結論(*): 對於2 * n = (3^k - 1),這種長度的數組,恰好只有k個圈,並且每個圈的頭部是1,3,9,...3^(k - 1)。

這樣我們就解決了這種特殊的n作為長度的問題。那麼,對於任意的n怎麼辦?我們利用演算法2的思路,把它拆成兩部分,前一部分是滿足結論(*)。後一部分再單獨算。

為了把數組分成適當的兩部分,我們同樣需要交換一些元素,但這時交換的元素個數不相等,不能簡單地迴圈交換,我們需要更強大的工具——迴圈移。

假設滿足結論(*)的需要的長度是2 * m = (3^k - 1), 我們需要把n分解成m和n - m兩部分,按下標來說,是這樣:

原先的數組(1..m) (m + 1.. n) (n + 1..n + m)(n + m + 1..2 * n)

我們要達到的數組 (1..m)(n + 1.. n + m)(m + 1..n)(n + m + 1..2  * n)

可見,中間那兩段長度為(n - m)和m的段需要交換位置,這個相當於把(m + 1..n + m)的段迴圈右移m次,而迴圈右移是有O(長度)的演算法的, 主要思想是把前(n - m)個元素和後m個元素都翻轉一下,再把整個段翻轉一下。

迴圈移位的代碼:

//翻轉字串時間複雜度O(to - from)void reverse(int *a,int from,int to) {int t;    for (; from < to; ++from, --to) {        t = a[from];        a[from] = a[to];        a[to] = t;    }    }//迴圈右移num位 時間複雜度O(n)void right_rotate(int *a,int num,int n) {    reverse(a, 1, n - num);    reverse(a, n - num + 1,n);    reverse(a, 1, n);}

再用a和b舉例一下,設n = 7這樣m = 4, k = 2

原先的數組是a1,a2,a3,a4,(a5,a6,a7),(b1,b2,b3,b4),b5,b6,b7。

結論(*)是說m = 4的部分可以直接搞定,

也就是說我們把中間加括弧的那兩段(a5,a6,a7) (b1,b2,b3,b4)交換位置,也就是把(a5,a6,a7,b1,b2,b3,b4)整體迴圈右移4位就可以得到:

(a1,a2,a3,a4,b1,b2,b3,b4)(a5,a6,a7,b5,b6,b7)

於是前m = 4個由演算法cycle_leading演算法直接搞定,n的長度減小了4。

所以這也是一種分治演算法。演算法流程:

輸入數組 a[1..2 * n]

step 1 找到 2 * m = 3^k - 1 使得 3^k <= 2 * n < 3^(k +1)

step 2 把a[m + 1..n + m]那部分迴圈移m位

step 3 對每個i = 0,1,2..k - 1,3^i是個圈的頭部,做cycle_leader演算法,數組長度為m,所以對2 * m + 1模數。

step 4 對數組的後面部分a[2 * m + 1.. 2 * n]繼續使用本演算法, 這相當於n減小了m。

時間複雜度分析:

(1) 因為迴圈不斷乘3的,所以時間複雜度O(logn)

  (2)    迴圈移位O(n)

  (3)   每個圈,每個元素只走了一次,一共2*m個元素,所以複雜度omega(m), 而m < n,所以 也在O(n)內。

(4) T(n - m)

因此 複雜度為 T(n) = T(n - m) + O(n)      m = omega(n)  解得:總複雜度T(n) = O(n)。

演算法代碼:

//時間O(n),空間O(1)void perfect_shuffle3(int *a,int n) {int n2, m, i, k,t;    for (;n > 1;) {        // step 1        n2 = n * 2;        for (k = 0, m = 1; n2 / m >= 3; ++k, m *= 3)        ;        m /= 2;        // 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1)                // step 2        right_rotate(a + m, m, n);                // step 3                for (i = 0, t = 1; i < k; ++i, t *= 3) {            cycle_leader(a , t, m * 2 + 1);                    }                //step 4        a += m * 2;        n -= m;        }    // n = 1    t = a[1];    a[1] = a[2];    a[2] = t;    }

以上解決了完美洗牌問題,對於這個題,我們可以在完美洗牌之前把a的前n項與後n項交換掉,再調用perfect_shuffle3演算法。

代碼:

//時間複雜度O(n),空間複雜度O(1),數組下標從1開始,調用perfect_shuffle3void shuffle(int *a,int n) {    int i,t;    for (i = 1; i <= n; ++i) {        t = a[i];        a[i] = a[i + n];        a[i + n] = t;            }    perfect_shuffle3(a,n);}

或者我們還可以先調用pefect_shuffle3演算法,再對每組的兩個相鄰元素做交換。

代碼:

//時間複雜度O(n),空間複雜度O(1),數組下標從1開始,調用perfect_shuffle3void shuffle(int *a,int n) {    int i,t,n2 = n * 2;    perfect_shuffle3(a,n);    for (i = 2; i <= n2; i += 2) {        t = a[i - 1];        a[i - 1] = a[i];        a[i] = t;            }}

無論採取哪種方法,都額外增加O(n)的時間複雜度,總時間複雜度不變,仍然是O(n),空間複雜度是O(1)。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.