[经典面试题]完美洗牌算法

题目

有个长度为2n的数组{a1,a2,a3,…,an,b1,b2,b3,…,bn},希望排序后{a1,b1,a2,b2,….,an,bn},请考虑有无时间复杂度o(n),空间复杂度0(1)的解法。

来源

2013年UC的校招笔试题

思路一

第①步、确定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已在最后的位置,不需要再交换。如此,经过上述3个步骤后,得到我们最后想要的序列。但此方法的时间复杂度为O(n^2)

代码一

/*---------------------------------------------
*   日期:2015-02-13
*   作者:SJF0115
*   题目: 完美洗牌算法
*   来源:2013年UC的校招笔试题
*   博客:
-----------------------------------------------*/
#include <iostream>
using namespace std;

class Solution {
public:
    void PerfectShuffle(int *A,int n){
        if(n <= 1){
            return;
        }//if
        //
        int size = 2*n;
        int index,count;
        for(int i = n;i < size;++i){
            // 交换个数
            count = n - (i - n) - 1;
            // 待交换
            index = i;
            for(int j = 1;j <= count;++j){
                swap(A[index],A[i-j]);
                index = i - j;
            }//for
        }//for
    }
};

int main() {
    Solution solution;
    int A[] = {1,2,3,4,5,6,7,8};
    solution.PerfectShuffle(A,4);
    for(int i = 0;i < 8;++i){
        cout<<A[i]<<" ";
    }//for
    cout<<endl;
}

思路二

我们每次让序列中最中间的元素进行两两交换。还是上面的例子:

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

此思路同上述思路一样,时间复杂度依然为O(n^2)。仍然但不到题目要求。

代码二

/*---------------------------------------------
*   日期:2015-02-13
*   作者:SJF0115
*   题目: 完美洗牌算法
*   来源:2013年UC的校招笔试题
*   博客:
-----------------------------------------------*/
#include <iostream>
using namespace std;

class Solution {
public:
    void PerfectShuffle(int *A,int n){
        if(n <= 1){
            return;
        }//if
        //
        int left = n - 1,right = n;
        // 交换次数
        for(int i = 0;i < n-1;++i){
            for(int j = left;j < right;j+=2){
                swap(A[j],A[j+1]);
            }//for
            --left;
            ++right;
        }//for
    }
};

int main() {
    Solution solution;
    int A[] = {1,2,3,4,5,6,7,8,9,10};
    solution.PerfectShuffle(A,5);
    for(int i = 0;i < 10;++i){
        cout<<A[i]<<" ";
    }//for
    cout<<endl;
}

思路三(完美洗牌算法)

玩过扑克牌的朋友都知道,在一局完了之后洗牌,洗牌人会习惯性的把整副牌大致分为两半,两手各拿一半对着对着交叉洗牌。

2004年,microsoft的Peiyush Jain在他发表一篇名为:“A Simple In-Place Algorithm for In-Shuffle”的论文中提出了完美洗牌算法。

什么是完美洗牌问题呢?即给定一个数组a1,a2,a3,…an,b1,b2,b3..bn,最终把它置换成b1,a1,b2,a2,…bn,an。这个完美洗牌问题本质上与本题完全一致,只要在完美洗牌问题的基础上对它最后的序列swap两两相邻元素即可。

(1)对原始位置的变化做如下分析:

(2)依次考察每个位置的变化规律:

a1:1 -> 2

a2:2 -> 4

a3:3 -> 6

a4:4 -> 8

b1:5 -> 1

b2:6 -> 3

b3:7 -> 5

b4:8 -> 7

对于原数组位置i的元素,新位置是(2*i)%(2n+1),注意,这里用2n表示原数组的长度。后面依然使用该表述方式。有了该表达式,困难的不是寻找元素在新数组中的位置,而是为该元素“腾位置”。如果使用暂存的办法,空间复杂度必然要达到O(N),因此,需要换个思路。

(3)我们这么思考:a1从位置1移动到位置2,那么,位置2上的元素a2变化到了哪里呢?继续这个线索,我们得到一个“封闭”的环:

1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1

沿着这个环,可以把a1、a2、a4、b4、b3、b1这6个元素依次移动到最终位置;显然,因为每次只移动一个元素,代码实现时,只使用1个临时空间即可完成。(即:a=t;t=b;b=a)

此外,该变化的另外一个环是:

3 -> 6 -> 3

沿着这个环,可以把a3、b2这2个元素依次移动到最终位置。

    // 走圈算法
    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;
        // 按环移动位置
        while(next != start){
            swap(pre,a[next]);
            next = 2 * next % mod;
        }//while
        a[start] = pre;
    }

(4)上述过程可以通过若干的“环”的方式完整元素的移动,这是巧合吗?事实上,该问题的研究成果已经由Peiyush Jain在10年前公开发表在A Simple In-Place Algorithm for In-Shuffle, Microsoft, 2004中。原始论文直接使用了一个结论,这里不再证明:对于2*n =(3^k-1)这种长度的数组,恰好只有k个环,且每个环的起始位置分别是1,3,9,…3^(k-1)。

对于上面的例子,长度为8,是3^2-1,因此,只有2个环。环的起始位置分别是1和3。

(5)至此,完美洗牌算法的“主体工程”已经完工,只存在一个“小”问题:如果数组长度不是(3^k-1)呢?

若2n!=(3^k-1),则总可以找到最大的整数m,使得m< n,并且2m=(3^k-1)。

对于长度为2m的数组,调用(3)和(4)中的方法整理元素,剩余的2(n-m)长度,递归调用(5)即可。

(6)需要交换一部分数组元素

(下面使用[a,b]表示从a到b的一段子数组,包括端点)

①图中斜线阴影部分的子数组[1,m]应该和[n + 1,n + m]组成一个数组,调用(3)和(4)中的算法;

②数组[m+1,m+n]循环左移n-m次即可。(循环位移是存在空间复杂度为O(1),时间复杂度为O(n)的算法)

(7)原始问题要输出a1,b1,a2,b2……an,bn,而完美洗牌却输出的是b1,a1,b2,a2,……bn,an。解决办法非常简单:忽略原数组中的a1和bn,对于a2,a3,……an,b1,b2,……bn-1调用完美洗牌算法,即为结论。

举个例子: n = 6

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

循环左移

介绍一下时间复杂度为O(n),空间复杂度为O(1)的循环移位操作。

思路:

假设循环左移m位。把数组分成两段,第一段为前m个元素,第二段为剩余元素。把第一段和第二段先各自翻转一下,再将整体翻转下。

    // 翻转 start 开始位置 end 结束位置
    void Reverse(int *a,int start,int end){
        while(start < end){
            swap(a[start],a[end]);
            ++start;
            --end;
        }//while
    }
    // 循环左移m位 n数组长度 下标从1开始
    void LeftRotate(int *a,int m,int n){
        // 翻转前m位
        Reverse(a,1,m);
        // 翻转剩余元素
        Reverse(a,m+1,n);
        // 整体翻转
        Reverse(a,1,n);
    }

引用:

http://blog.csdn.net/v_july_v/article/details/10212493

http://ask.julyedu.com/question/33

http://blog.csdn.net/caopengcs/article/details/10521603

http://cs.stackexchange.com/questions/332/in-place-algorithm-for-interleaving-an-array/400#400

时间: 2024-10-12 20:05:21

[经典面试题]完美洗牌算法的相关文章

【转】完美洗牌算法

转自:https://yq.aliyun.com/articles/3575 题目 有个长度为2n的数组{a1,a2,a3,…,an,b1,b2,b3,…,bn},希望排序后{a1,b1,a2,b2,….,an,bn},请考虑有无时间复杂度o(n),空间复杂度0(1)的解法. 来源 2013年UC的校招笔试题 思路一 第①步.确定b1的位置,即让b1跟它前面的a2,a3,a4交换: a1,b1,a2,a3,a4,b2,b3,b4 第②步.接着确定b2的位置,即让b2跟它前面的a3,a4交换: a

完美洗牌算法

本文是看完july博客完美洗牌之后的个人笔记. 题目:把a1,a2,a3,a4,...,an-1 an,b1,b2,b3,...,bn-1,bn变成a1,b1,a2,b1,...,an,bn.要求时间复杂度为O(n),空间复杂度为O(1). 1.位置置换算法:b是新开的一个数组,但是时间复杂度为O(n),空间复杂度为O(n). void perfectShuttle(int a[],int n){ int n2=n*2,b[N]; for(int i=1;i<=n2;i++) b[(2*i)%(

扑克牌的完美洗牌算法

思路: 递归思想.我们有n张牌,不妨先假设有一个洗牌函数shuffle(....),能完美的洗出n-1张牌 .拿第n张牌来打乱前面n-1的洗牌顺序,从而得到n张牌的最终结果. 代码如下: 1 #include <iostream> 2 #include <cstdlib> 3 using namespace std; 4 5 //随机指定区域内的数 6 int MyRand(int low, int high) 7 { 8 return low + rand() % (high -

算法系列:完美洗牌算法

给定一个数组a1,a2,a3,...an,b1,b2,b3..bn,最终把它置换成b1,a1,b2,a2,...bn,an. http://arxiv.org/pdf/0805.1598.pdf https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/02.09.md

[转]完美洗牌(Perfect Shuffle)问题

[转]原博文地址:https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/02.09.md 完美洗牌算法 题目详情 有个长度为2n的数组{a1,a2,a3,...,an,b1,b2,b3,...,bn},希望排序后{a1,b1,a2,b2,....,an,bn},请考虑有无时间复杂度o(n),空间复杂度0(1)的解法. 题目来源:此题是去年2013年UC的校招笔试题,看似简单,按照题目所要

完美洗牌算饭

被大腾讯问到了完美洗牌算法,瞬间就跪了,其实原来看过,只可惜都忘了啊,现在在补充进来吧. 其实完美洗牌算法,应该给我说明白题,最少举个例子吧,当时确实大意了,也没问清楚就直接不会了,其实题意是有个长度为2n的数组{a1,a2,a3,a4,..,an,b1,b2,b3,b4,...,bn},希望排序后{a1,b1,a2,b2,...,an,bn},最好要求时间复杂度为O(n),空间复杂度为O(1). 解法一:蛮力交换方法 1.1 说白了就是b1和a2,a3,a4,...,an交换,然后是b2和a3

转:浅谈洗牌算法(面试题)

很多人都有耳闻过洗牌算法,时常会在面试中碰到,我们下面来定义一下这个问题. 所谓洗牌算法,就是给你一个1到n的序列,让你随机打乱,保证每个数出现在任意一个位置的概率相同,也就是说在n!个的排列中,每一个排列出现的概率相同. 最朴素的做法 对于这个问题我们从最朴素的解法谈起.每次随机选出一个没有被选过的数放到一个队列中,如果随机出来的数已经被选过,那么继续随机直到遇到一个没有被选过的数放入到队列中:重复这样子操作直到所有的数都被选择出来. 我们看看这样子作为什么是对的.首先选第一个数的时候有n个数

(算法)完美洗牌

题目: 编写一个方法,洗一副牌,要求做到完美洗牌,即这副牌52!中排列组合出现的概率相同. 思路: 1.递归 2.循环 代码: #include<iostream> #include<stdlib.h> #include<time.h> using namespace std; int rnd(int lower,int higher){ return rand()%(higher-lower+1)+lower; } void shuffle_1(int *cards,

(笔试题)洗牌算法

题目: 给定N张扑克牌和一个随机函数,设计一个洗牌算法 思路: 假设数组A存的是扑克牌代表的数字,则洗牌的过程就是数组中元素交换的过程. 洗牌是个随机的过程,也是一个排列组合的过程. 假设有N张牌,则其排列组合的可能情况为N!=N*(N-1)*....*2*1. 有两种的随机洗牌函数: 1.A[i]=A[rand()%N] 2.A[i]=A[rand()%(N-i)] 但第一种情况得到的选择可能为N^N,而第二种情况为N! 因此第二种随机算法更符合. 代码: void shuffle(int a