完美洗牌算法

完美洗牌算法

題目描述:

有個長度為2n的數(shù)組 {a1, a2, a3, ..., an, b1, b2, b3, ..., bn} 调缨,希望排序后 {a1, b1, a2, b2, ...., an, bn} 词顾,請考慮有無時間復雜度 O(n),空間復雜度 O(1) 的解法贷盲。

分析和解法:

解法一:蠻力變換

題目要我們怎么變換,咱們就怎么變換。為了便于分析,我們?nèi)?n = 4丁眼,那么題目要求我們把

a1,a2昭殉,a3苞七,a4, b1挪丢,b2蹂风,b3,b4

變成

a1乾蓬,b1惠啄,a2,b2,a3礁阁,b3,a4族奢,b4

1.1姥闭、步步前移

仔細觀察變換前后兩個序列的特點,我們可做如下一系列操作:

第①步越走、確定 b1 的位置棚品,即讓 b1 跟它前面的 a2,a3廊敌,a4 交換:

a1铜跑,b1,a2骡澈,a3锅纺,a4, b2肋殴,b3囤锉,b4

第②步、接著確定 b2 的位置护锤,即讓 b2 跟它前面的 a3官地,a4 交換:

a1,b1烙懦,a2驱入,b2,a3氯析,a4亏较, b3,b4

第③步魄鸦、b3 跟它前面的 a4 交換位置:

a1宴杀,b1,a2拾因,b2旺罢,a3,b3绢记,a4扁达,b4

b4 已在最后的位置,不需要再交換蠢熄。如此跪解,經(jīng)過上述 3 個步驟后,得到我們最后想要的序列签孔。

源代碼如下:

#include <iostream>

using namespace std;

void Move(int a[], int n)
{
    if (n < 3 || n % 2 == 1)
        return;
    
    int size = n / 2;  //規(guī)模 
    int index, count;
    for (int i = size; i < n - 1; i++)
    {
        count = size - (i - size) - 1;  //交換個數(shù) 
        index = i;  //待交換的數(shù)的下標 
        for (int j = 1; j <= count; j++)
        {
            swap(a[index], a[i - j]);
            index = i - j;
        }
    }
}

int main()
{
    int a[100];
    int n = 0;
    while(cin.peek() != '\n')  cin >> a[n++];
    Move(a, n);
    for (int i = 0; i < n; i++)
        cout << a[i] << " " ;
    cout << endl;
    return 0;
}

分析:但此方法的時間復雜度為 O(N^2)叉讥,我們得繼續(xù)尋找其它方法窘行,看看有無辦法能達到題目所預期的 O(N)的時間復雜度。

1.2图仓、中間交換

當然罐盔,除了如上面所述的讓 b1,b2救崔,b3惶看,b4 步步前移跟它們各自前面的元素進行交換外,我們還可以每次讓序列中最中間的元素進行交換達到目的六孵。還是用上面的例子纬黎,針對 a1,a2劫窒,a3本今,a4,b1主巍,b2诈泼,b3,b4

第①步:交換最中間的兩個元素 a4煤禽,b1铐达,序列變成:

a1,a2檬果,a3 瓮孙,b1,a4选脊, b2杭抠,b3,b4

第②步恳啥,讓最中間的兩對元素各自交換:

a1偏灿,a2 ,b1钝的,a3翁垂,b2,a4硝桩, b3沿猜,b4

第③步,交換最中間的三對元素碗脊,序列變成:

a1啼肩,b1,a2,b2祈坠,a3害碾,b3,a4赦拘,b4

同樣蛮原,此法同解法 1.1、步步前移一樣另绩,時間復雜度依然為 O(N^2)。

源代碼如下:

#include <iostream>

using namespace std;

void Move(int a[], int n)
{
    if (n < 3 || n % 2 == 1)
        return;
        
    int size = n / 2;
    int count = 1;
    for (int i = size; i > 1; i--)
    {
        for (int j = size - count; j < size + count; j += 2)
        {
            swap(a[j], a[j + 1]);   
        }   
        count++;
    }   
} 

int main()
{
    int a[100];
    int n = 0;
    while(cin.peek() != '\n')  cin >> a[n++];
    Move(a, n);
    for (int i = 0; i < n; i++)
        cout << a[i] << " " ;
    cout << endl;
    return 0;
}

分析:此思路同上述思路一樣花嘶,時間復雜度依然為 O(n^2)笋籽,仍然達不到題目要求。

解法二:完美洗牌算法

玩過撲克牌的朋友都知道椭员,在一局完了之后洗牌车海,洗牌人會習慣性的把整副牌大致分為兩半,兩手各拿一半對著對著交叉洗牌隘击。

2004年侍芝,microsoft 的 Peiyush Jain 在他發(fā)表一篇名為:“A Simple In-Place Algorithm for In-Shuffle” 的論文中提出了完美洗牌算法。

什么是完美洗牌問題呢埋同?即給定一個數(shù)組

a1州叠,a2,a3凶赁, …咧栗, an, b1虱肄, b2致板, b3, ...咏窿, bn

最終把它置換成

b1斟或, a1, b2集嵌, a2萝挤, a3, b3根欧,…平斩, bn, an

這個完美洗牌問題本質(zhì)上與本題完全一致咽块,只要在完美洗牌問題的基礎(chǔ)上對它最后的序列 swap 兩兩相鄰元素即可绘面。

2.1、位置置換 perfect_shuffle1 算法

(1)對原始位置的變化做如下分析:
位置變化
(2)依次考察每個位置的變化規(guī)律:

從上面的例子我們能看到,前 n 個元素中揭璃,

第 1 個元素 a1 到了原第 2 個元素 a2 的位置晚凿,即 1 -> 2;
第 2 個元素 a2 到了原第 4 個元素 a4 的位置瘦馍,即 2 -> 4歼秽;
第 3 個元素 a3 到了原第 6 個元素 b2 的位置,即 3 -> 6情组;
第 4 個元素 a4 到了原第 8 個元素 b4 的位置燥筷,即 4 -> 8;

那么推廣到一般情況即是:前 n 個元素中院崇,第 i 個元素去了 第(2 * i)的位置肆氓。

上面是針對前 n 個元素,那么針對后 n 個元素底瓣,可以看出:

第 5 個元素 b1 到了原第 1 個元素 a1 的位置谢揪,即 5 -> 1;
第 6 個元素 b2 到了原第 3 個元素 a3 的位置捐凭,即 6 -> 3拨扶;
第 7 個元素 b3 到了原第 5 個元素 b1 的位置,即 7 -> 5茁肠;
第 8 個元素 b4 到了原第 7 個元素 b3 的位置患民,即 8 -> 7;

推廣到一般情況是:后 n 個元素垦梆,第 i 個元素去了第 (2 * (i - n) ) - 1 = 2 * i - (2 * n + 1) = (2 * i) % (2 * n + 1) 的位置酒奶。

當 0 < i < n 時, 原式 = (2 * i) % (2 * n + 1) = 2 * i奶赔;
當 i > n 時惋嚎,原式 (2 * i) % (2 * n + 1) 保持不變。

再綜合到任意情況:任意的第 i 個元素站刑,我們最終換到了 (2 * i) % (2 * n + 1) 的位置另伍。

因此,如果題目允許我們再用一個數(shù)組的話绞旅,我們直接把每個元素放到該放得位置就好了摆尝。也就產(chǎn)生了最簡單的方法 perfect_shuffle1 。

源代碼如下:

#include <iostream>

using namespace std;

void PerfectShuffle1(int a[], int n)
{
    if (n < 3 || n % 2 == 1)
        return;
        
    int b[n];
    for (int i = 1; i < n - 1; i++)
        b[(i * 2) % (n - 1)] = a[i];
    for (int j = 1; j < n - 1; j++)
        a[j] = b[j];
}

int main()
{
    int a[100];
    int n = 0;
    while(cin.peek() != '\n')  cin >> a[n++];
    PerfectShuffle1(a, n);
    for (int i = 0; i < n; i++)
        cout << a[i] << " " ;
    cout << endl;
    return 0;
}

分析:它的時間復雜度雖然是 O(n)因悲,但其空間復雜度卻是 O(n)堕汞,仍不符合本題所期待的時間 O(n),空間 O(1) 晃琳。我們繼續(xù)尋找更優(yōu)的解法讯检。

2.2琐鲁、完美洗牌算法 perfect_shuffle2

2.2.1、走圈算法 cycle_leader

根據(jù)上面變換的節(jié)奏人灼,我們可以看出有兩個圈

一個是1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1围段;
一個是3 -> 6 -> 3。

這兩個圈可以表示為(1,2,4,8,7,5)和(3,6)投放,且 perfect_shuffle1 算法也已經(jīng)告訴了我們奈泪,不管 n 是奇數(shù)還是偶數(shù),每個位置的元素都將變?yōu)榈冢?*i) % (2n+1)個元素:

因此我們只要知道圈里最小位置編號的元素即圈的頭部灸芳,順著圈走一遍就可以達到目的涝桅,且因為圈與圈是不相交的,所以這樣下來烙样,我們剛好走了 O(N)步冯遂。

2.2.2、神級結(jié)論:若 2 * n =(3^k - 1)误阻,則可確定圈的個數(shù)及各自頭部的起始位置

下面我要引用此論文 “A Simple In-Place Algorithm for In-Shuffle” 的一個結(jié)論了,即 對于 2 * n = (3^k-1)這種長度的數(shù)組晴埂,恰好只有 k 個圈究反,且每個圈頭部的起始位置分別是 1,3儒洛,9精耐,...,3^(k-1)琅锻。

至此卦停,完美洗牌算法的 “主體工程” 已經(jīng)完工,只存在一個 “小” 問題:如果數(shù)組長度不是(3^k - 1)呢恼蓬?

若 2 * n != (3^k - 1)惊完,則總可以找到最大的整數(shù) m,使得 m < n处硬,并且 2 * m = (3^k - 1)小槐。

對于長度為 2 * m 的數(shù)組,整理元素后剩余的 2 *(n - m)長度荷辕,遞歸調(diào)用完美洗牌算法即可凿跳。

2.2.3、完美洗牌算法 perfect_shuffle3

從上文的分析過程中也就得出了我們的完美洗牌算法疮方,其算法流程為:

輸入數(shù)組 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]那部分循環(huán)移 m 位
step 3 對每個 i = 0,1,2..k - 1控嗜,3^i 是個圈的頭部,做 cycle_leader 算法骡显,數(shù)組長度為 m疆栏,所以對 2 * m + 1 取模曾掂。
step 4 對數(shù)組的后面部分 a[2 * m + 1.. 2 * n] 繼續(xù)使用本算法, 這相當于 n 減小了 m 。

2.2.4承边、perfect_shuffle2 算法解決其變形問題

原始問題要輸出 a1, b1, a2, b2……an, bn遭殉,而完美洗牌卻輸出的是b1, a1, b2, a2,……bn, an。解決辦法非常簡單:交換兩兩相鄰元素即可(當然博助,你也可以讓原數(shù)組第一個和最后一個不變险污,中間的 2 * (n - 1) 項用原始的標準完美洗牌算法做),只是在完美洗牌問題時間復雜度 O(N) 空間復雜度 O(1) 的基礎(chǔ)上再增加 O(N) 的時間復雜度富岳,故總的時間復雜度 O(N) 不變蛔糯,且理所當然的保持了空間復雜度 O(1) 。至此窖式,咱們的問題得到了圓滿解決蚁飒!

源代碼如下:

#include <iostream>
using namespace std;

class Solution
{
    public:
        // 完美洗牌算法
        void PerfectShuffle(int *a, int n)
        {
            while(n >= 1)
            {
                // 計算環(huán)的個數(shù)
                int k = 0;
                // 3^1
                int r = 3;
                // 2 * m  = 3^k - 1
                // m <= n  ->  2 * m <= 2 * n  -> 3^k - 1 <= 2 * n
                // 尋找最大的k使得3^k - 1 <= 2*n
                while(r - 1 <= 2 * n)
                {
                    r *= 3;
                    ++k;
                }//while
                int m = (r / 3 - 1) / 2;
                // 循環(huán)左移n-m位
                LeftRotate(a + m, n - m, n);
                // k個環(huán) 環(huán)起始位置start: 1,3...3^(k-1)
                for(int i = 0, start = 1; i < k; ++i, start *= 3)
                {
                    // 走圈
                    CycleLeader(a, start, m);
                }//for
                a += 2 * m;
                n -= m;
            }
        }
    private:
        // 翻轉(zhuǎn) start 開始位置 end 結(jié)束位置
        void Reverse(int *a, int start, int end)
        {
            while(start < end)
            {
                swap(a[start], a[end]);
                ++start;
                --end;
            }//while
        }
        // 循環(huán)右移m位 n數(shù)組長度 下標從1開始
        void LeftRotate(int *a, int m, int n)
        {
            // 翻轉(zhuǎn)前m位
            Reverse(a, 1, m);
            // 翻轉(zhuǎn)剩余元素
            Reverse(a, m + 1, n);
            // 整體翻轉(zhuǎn)
            Reverse(a, 1, n);
        }
        // 走圈算法
        void CycleLeader(int *a, int start, int n)
        {
            int pre = a[start];
            // 2 * i % (2 * n + 1)
            int mod = 2 * n + 1;
            // 實際位置
            int next = start * 2 % mod;
            // 按環(huán)移動位置
            while(next != start)
            {
                swap(pre,a[next]);
                next = 2 * next % mod;
            }//while
            a[start] = pre;
        }
};


int main()
{
    Solution solution;
    int a[100];
    int n = 0;
    while(cin.peek() != '\n')  cin >> a[++n];
    solution.PerfectShuffle(a, n/2);
    for (int i = 1; i <= n; i += 2)
        swap(a[i], a[i + 1]);
    for (int i = 1; i <= n; i++)
    {
        cout << a[i] << " " ;
    }//for
    cout << endl;
    return 0;
}

分析:時間復雜度為 O(n),空間復雜度為 O(1)萝喘。

特別注意:

本次題目比較簡單淮逻,但是要求限制有點苛刻。完美洗牌算法和神級結(jié)論還需細致的了解阁簸。

參考資料:《編程之法》The Art of Programming By July
[經(jīng)典面試題]完美洗牌算法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末爬早,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子启妹,更是在濱河造成了極大的恐慌筛严,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饶米,死亡現(xiàn)場離奇詭異桨啃,居然都是意外死亡,警方通過查閱死者的電腦和手機檬输,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門照瘾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丧慈,你說我怎么就攤上這事网杆。” “怎么了伊滋?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵碳却,是天一觀的道長。 經(jīng)常有香客問我笑旺,道長昼浦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任筒主,我火速辦了婚禮关噪,結(jié)果婚禮上鸟蟹,老公的妹妹穿的比我還像新娘。我一直安慰自己使兔,他們只是感情好建钥,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著虐沥,像睡著了一般熊经。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上欲险,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天镐依,我揣著相機與錄音,去河邊找鬼天试。 笑死槐壳,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的喜每。 我是一名探鬼主播务唐,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼带兜!你這毒婦竟也來了枫笛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤鞋真,失蹤者是張志新(化名)和其女友劉穎崇堰,沒想到半個月后沃于,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涩咖,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年繁莹,在試婚紗的時候發(fā)現(xiàn)自己被綠了檩互。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡咨演,死狀恐怖闸昨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情薄风,我是刑警寧澤饵较,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站遭赂,受9級特大地震影響循诉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撇他,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一茄猫、第九天 我趴在偏房一處隱蔽的房頂上張望狈蚤。 院中可真熱鬧,春花似錦划纽、人聲如沸脆侮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽靖避。三九已至,卻和暖如春芭毙,著一層夾襖步出監(jiān)牢的瞬間筋蓖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工退敦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粘咖,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓侈百,卻偏偏與公主長得像瓮下,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钝域,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容