算法设计与分析基础系列--生成排列与洗牌算法(二)

转载文章,原文章来源于算法设计与分析基础系列--生成排列与洗牌算法(二)(欢迎关注微信公众号,会定期更新内容)

============================================================

前言

本文内容基于书籍"算法设计与分析基础"(Introduction to The Design and Analysis of Algorithms,作者Anany Levitin),主要学习和讨论其中的生成排列与洗牌算法。在“算法设计与分析基础系列--生成排列与洗牌算法(一)”中,我们重点讨论了生成排列的算法,那么在有了排列生成算法之后,我们现在可以来看看"洗牌算法"了。

问题描述

洗牌问题的一般描述是给定n张不同的牌,设计一个函数随机均匀地生成其中一种排列,即以概率1/n!生成其中一种排列结果,有时也会以面试题的形式出现。

解答

洗牌问题和排列生成问题基本一样,可以认为是给定n个不同的数字,请以等概率生成其中一种排列。

首先我们要记得,在C++中,有一个函数rand(),它以等概率生成0到某个最大值maxn之间的一个数。如果我们想以等概率从0到n-1之间选择一个数的话,那么可以通过rand() % n来实现。虽然从数学上来说这并不是严格等概率的(主要取决于maxn是否是n的倍数,如果不是,那么总会有某些数的概率稍稍大于其他数),但我们先不去考虑这些细节,而是假设我们存在一个函数calrand(n),它可以以等概率从0到n-1之间选择一个数。

有了上述"等概率"函数,我们可以想到一个比较直接的算法,就是生成所有的排列,假设有t种,并按照0,1,...,t-1进行编号,那么我们可以利用calrand(t)来等概率地选择出一种排列,从而解决该问题。

但是上述算法存在一个严重缺陷,就是其复杂度非常高。对于n个数的情况,其所有排列的结果有t=n!种,当n=10的时候,t的取值已经在百万量级了,随着n的增加,该算法的复杂度将极速增加从而导致不可用。因此,我们还需另想办法。

我们还是仿照之前排列生成算法的思想,从第一个位置开始逐个确定每个位置放置哪个数字。

首先看第一个位置,它有n种可选的数字,如果我们以等概率从n个数字中选择一个的话,假设为a[0],那么选择a[0]的概率就是1/n。接着需要确定第二个位置,由于不能和第一个位置取相同的数字,因此第二个位置有n-1种选择,如果我们以等概率从这n-1个数字中选择一个的话,假设为a[1],那么选择a[1]的概率就是1/(n-1)。同理,对于第三个位置还有n-2种选择,我们以等概率进行选择,假设为a[2],那么选择a[2]的概率就是1/(n-2),以此类推,直到我们确定所有n个位置的数字。

现在,我们来处理一些数学细节,从数学上证明上述算法以等概率生成任意一种排列(如果作为面试题,也可能会问到其数学原理)。

记p(a[0],a[1],...,a[n-1])表示生成最终结果为a[0],a[1],...,a[n-1]的概率,且记条件概率p(i)=p(a[i] | a[0],a[1],...,a[i-1]),即在已知前i个数字的条件下,第i+1个数字取a[i]的概率。根据贝叶斯条件概率公式可得

p(a[0],a[1],...,a[n-1])

=p(a[0],a[1],...,a[n-2])*p(a[n-1] | a[0],a[1],...,a[n-2])

=p(a[0],a[1],...,a[n-2])*p(n-1)

=p(a[0],a[1],...,a[n-3])*p(n-2)*p(n-1)

=...

=p(0)*p(1)*...*p(n-1)

而p(i)=p(a[i] | a[0],a[1],...,a[i-1])=1/(n-i),这是因为在前i个数字确定的条件下,第i+1个数字还有n-i种选择,而我们会以等概率选择其中一个。

因此有p(a[0],a[1],...,a[n-1])=1/(n*(n-1)*...*1)=1/n!

即生成每个排列结果的概率都相等,均为1/n!

实现代码如下,其中calrand(n)需要自己实现一下


int a[n]; // 数组a包含数字从0到n-1 即a[i]=i

void solve(int n){

for(int i = 0; i < n; i++){ // 确定每个位置放置的数字

       int j = calrand(n - i); // 已经确定了i个数字,现在需要对剩下的n-i个数字以等概率选择一个,记得文章开头提到过用calrand(n)表示以等概率从0到n-1中选择一个数字

       int k = i + j; // 注意,上述等概率选择一个数字其实就是等概率对[i,i+1,i+2,...,n-1]这些索引中等概率选择一个,而j是从[0,1,2,...,n-i-1]中等概率选择一个,因此只要加上i(即k=i+j)就相当于从[i,i+1,i+2,...,n-1]中等概率选择一个了。

       int tmp = a[i]; // 下面3行代码就是交换a[i]和a[k],因为k是从[i,i+1,i+2,...,n-1]中等概率选择的,所以交换两者就是以概率1/(n-i)选择一个数字

       a[i] = a[k];

       a[k] = tmp;

   }

}

int main(){ // 下面是使用方法

   n = 6;

   for(int i = 0; i < n; i++){

       a[i] = i;

   }

   solve(n);

}

总结

1,洗牌算法的代码框架和排列生成基本一致,只要引入一个随机函数即可。

2,如果是在面试中,除了想到算法和完成代码,能够进行一些数学推理应该是个很好的加分项,说明面试者对其原理的理解是比较深入的。

欢迎大家多多转载并关注后续更新,如果有其他算法相关的问题也欢迎留言讨论~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值