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

简介:

题目

有个长度为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);
    }

代码:

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

class Solution {
public:
    // 完美洗牌算法
    void PerfectShuffle(int *a,int n){
        while(n >= 1){
            // 计算环的个数
            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;
            // 循环左移n-m位
            LeftRotate(a+m,n-m,n);
            // k个环 环起始位置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:
    // 翻转 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);
    }
    // 走圈算法
    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;
    }
};


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

拓展一

问题:如果输入是a1,a2,……an, b1,b2,……bn, c1,c2,……cn,要求输出是c1,b1,a1,c2,b2,a2,……cn,bn,an怎么办?
分析: 这个问题本质上其实还是上面的完美洗牌算法一样,我们一样还是分析其规律。

这里写图片描述

对于原数组位置i的元素,新位置是(3*i)%(3n+1)

这里写图片描述
这里写图片描述

图中所说的步骤三四五和上面的三四五大体一样,只是细节不太一样,看图就明白了。

引用:
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

目录
相关文章
|
6月前
|
算法
面试场景题:如何设计一个抢红包随机算法
本文详细解析了抢红包随机算法的设计与实现,涵盖三种解法:随机分配法、二倍均值法和线段切割法。随机分配法通过逐次随机分配金额确保总额不变,但易导致两极分化;二倍均值法优化了金额分布,使每次抢到的金额更均衡;线段切割法则将总金额视为线段,通过随机切割点生成子金额,手气最佳金额可能更高。代码示例清晰,结果对比直观,为面试中类似算法题提供了全面思路。
1151 17
|
负载均衡 NoSQL 算法
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
这篇文章是关于Java面试中Redis相关问题的笔记,包括Redis事务实现、集群方案、主从复制原理、CAP和BASE理论以及负载均衡算法和类型。
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
|
6月前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
6月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
[go 面试] 雪花算法与分布式ID生成
[go 面试] 雪花算法与分布式ID生成
|
11月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩分享分库分表的基因算法设计,涵盖分片键选择、水平拆分策略及基因法优化查询效率等内容,助力面试者应对大厂技术面试,提高架构设计能力。
美团面试:百亿级分片,如何设计基因算法?
|
11月前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩在读者群中分享了关于分库分表的基因算法设计,旨在帮助大家应对一线互联网企业的面试题。文章详细介绍了分库分表的背景、分片键的设计目标和建议,以及基因法的具体应用和优缺点。通过系统化的梳理,帮助读者提升架构、设计和开发水平,顺利通过面试。
美团面试:百亿级分片,如何设计基因算法?
|
11月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
867 2
|
11月前
|
存储 算法 安全
ArrayList简介及使用全方位手把手教学(带源码),用ArrayList实现洗牌算法,3个人轮流拿牌(带全部源码)
文章全面介绍了Java中ArrayList的使用方法,包括其构造方法、常见操作、遍历方式、扩容机制,并展示了如何使用ArrayList实现洗牌算法的实例。
105 1
|
12月前
|
机器学习/深度学习 JavaScript 算法
面试中的网红虚拟DOM,你知多少呢?深入解读diff算法
该文章深入探讨了虚拟DOM的概念及其diff算法,解释了虚拟DOM如何最小化实际DOM的更新,以此提升web应用的性能,并详细分析了diff算法的实现机制。

热门文章

最新文章