c语言经典面试题 洗牌,网易游戏面试题:如何设计一个公平的洗牌算法

原标题:网易游戏面试题:如何设计一个公平的洗牌算法

f60a4d06b6b7e49e571ca53932b70d39.png

来源:景禹

过年期间,大家一定体验过各式各样的斗地主,但是你知道每一次发牌前后台是如何洗牌的吗?(难道如下图一样,模拟手动洗牌?)

2a04033b6776f44a3968339fa4bcbd8c.png

当然不是,但所有斗地主背后的洗牌原理都是一样的,那就是 Knuth 洗牌算法(算法的魅力

58d515c78d2ff47709825424e83f2889.png

)。

我记得在面试网易游戏的时候,面试官当时是这样问我的:“给你一副扑克牌,你能设计一个公平的洗牌算法吗?”。

我当时一懵,只想到了 rand 函数,用来生成一个随机数,就给面试官说:“我首先会 0 到 n-1 遍历给定的数组元素,然后每次生成一个随机数 rand % (n - i) ,然后将第 i 个元素与下标为随机数的元素交换,这样是不是可以呢?”

大致代码如下:

importjava.util.Random;

classShuffleCards{

// 洗牌算法

publicstaticvoidshuffle( intcard[], intn)

{

Random rand = newRandom;

for( inti = 0; i < n; i++)

{

// 生成一个[i, 52-i]的随机数

intj = i + rand.nextInt( 52- i);

//交换元素

inttemp = card[j];

card[j] = card[i];

card[i] = temp;

}

}

publicstaticvoidmain(String[] args)

{

// 纸牌编号

inta[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8,

9, 10, 11, 12, 13, 14, 15,

16, 17, 18, 19, 20, 21, 22,

23, 24, 25, 26, 27, 28, 29,

30, 31, 32, 33, 34, 35, 36,

37, 38, 39, 40, 41, 42, 43,

44, 45, 46, 47, 48, 49, 50, 51};

shuffle(a, 52);

for( inti = 0; i < 52; i ++)

System.out.print(a[i] + " ");

}

}

这里给大家提供的是 Java 版本的, rand.nextInt(52-i) ,换成 C 语言就是 rand % (52-i) 。

但这不是关键,关键面试官是否认同,他的回答很肯定:“当然可以,但是为什么你这样做就是公平的呢?”。

我当时其实是糊涂的,就说:“ 因为每一次使用 i + rand % (52 - i) 选中每一张牌是等概率的,即 1/52 。”

当面试官听到我这样的回答,其实是面露笑容的,但当他让我解释时,我还是跪了,不过面试官也没有为难我,那次面试还是顺利通过了。

后来我复盘时发现,原来这就是著名的 Knuth 算法,不过我当时纯属瞎猫碰上死耗子,命好!

细想一下,面试官问的是设计一个 公平的洗牌算法?

这里的公平究竟是什么意思呢?对于一副扑克牌共有 52 张,假设每一张都有唯一的编号,一个公平的洗牌算法应该是等概率地给出一副扑克牌所有排列中的任意一个,按照这样的思路,一副扑克牌可以组成的排列数为 52!个,随机取一个排列就可以了,但是这个思路计算机并不能很好地处理呀!

与汉诺塔一般,64! 次移动,每次移动假设为 1 秒,移动完所有盘子总共将耗费 5845.42 亿年,想一想,生成 52 张扑克牌的所有排列耗费的时间也是无法想象的。所以,这样的思路可以 Pass 掉了。

这里的公平,我们其实可以理解为, 对于生成的排列,每一个元素都能等概率地出现在每一个位置。或者说, 每一个位置都能等概率地放置每一个元素。这就是 Knuth 洗牌算法的核心思想。

对于生成的排列,每一个元素都能等概率地出现在每一个位置为何意呢?其实就是我面试“蒙”的那句话,每一次选中每一张扑克牌的概率都是 1/52。

52 这个数字对于解释可能太过复杂,我们从抛硬币开始,再回到洗牌。

排除零和的情况,正面朝上的概率为 1/2,反面朝上的概率也是 1/2。上面这句话你就可以理解为抛一枚硬币正面的可能性为 1/2,不是正面的可能性为(1 - 1/2 = 1/2)。

902caf4d45cc5c38595b0706b054f53d.png

《功夫足球》中的截图,自己悟

增加一下难度,假设一个袋子里有八个不同编号的球,请问你抓到编号为 1 的球的概率为多少?抓到编号不是 1 的概率又是多少?

be71de2e1cd1137aff8f4828065f0ab5.png

答:1/8 和 1 - 1/8 = 7/8 。

再增加一点儿难度,假设我们第一次随机抓到编号为 1 的球,那么第二次取到编号为 [2,3,4,5,6,7,8] 的任意一个球的概率是多少呢?

19580bdce68e739f462f0202cc34210e.png

答: 。

推而广之,假设有 n 个数,用这 n 个数生成的一个排列,任意一个位置存放元素的概率都是 1/n.

比如:第一个元素的概率为 1/n ,最后一个元素的概率为

代码也是相当简单.

importjava.util.Random;

importjava.util.Arrays;

publicclassShuffleRand

{

staticvoidrandomize( intarr[], intn)

{

Random r = newRandom;

for( inti = n- 1; i > 0; i--) {

intj = r.nextInt(i+ 1); // 选择一个 0,i+1 的随机数

inttemp = arr[i];

arr[i] = arr[j];

arr[j] = temp;

}

System.out.println(Arrays.toString(arr));

}

publicstaticvoidmain(String[] args)

{

int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8};

intn = arr.length;

randomize (arr, n);

}

}

这也是大家最常见的一种实现方式,实现图解如下:

f768c98d6d1032a28179773457f35e12.png

责任编辑:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值