点击上方蓝色字体,关注我 ——
一个在阿里云打工的清华学渣!
有一次参加面试,面试官问我:“会玩牌吧?”
内心:“咋滴,这是要玩德州扑克(或者炸金花),赢了他就能通过面试么?”
结果……
没想到面试官的下一句话:“给我讲讲洗牌算法以及它的应用场景吧!”
背景
本文产生背景是看到了 一枝花算不算浪漫
同学的这篇 Eureka注册中心集群如何实现客户端请求负载及故障转移?文章想到的。其实本人觉得那篇文中提到的负责均衡的重点就是本文要说的洗牌算法。
好了,回到题目上来。
这确实也是一道面试题,我曾经多次面试中都有遇到这个题目或者这个题目的变种。
你不妨花 1 秒,想想?
什么是洗牌算法
从名字上来看,就是给你一副牌让你洗呗,用怎样的方法才能洗得均匀呢?请大佬表演一下。
其实洗牌算法就是一种随机算法,你在斗地主的时候,随机把牌的顺序打乱就行。一个足够好的洗牌算法最终结果应该是可以让牌的顺序足够随机。好像有点绕~
这么来说吧,一副牌大家斗地主的话用 54 张(不考虑你们打配配牌的情形哈),那么这 54 张牌的顺序的话,按照排列组合算法,应该是有 54!
这么多种,然后你的洗牌算法就是从这 54!
种排列中,随机选 1 种。
无聊的石头算了一下,54 的阶乘有多大呢?大概就是这么大一长串数字,2308436973392413804720927448683027581083278564571807941132288000000000000L
,准确答案看下图:
我们还是以 4 张牌作为例子吧。
4 张牌,JQKA
,所有的排列有 4!=4*3*2*1=24
种,分别如下:
('J', 'Q', 'K', 'A')
('J', 'Q', 'A', 'K')
('J', 'K', 'Q', 'A')
('J', 'K', 'A', 'Q')
('J', 'A', 'Q', 'K')
('J', 'A', 'K', 'Q')
('Q', 'J', 'K', 'A')
('Q', 'J', 'A', 'K')
('Q', 'K', 'J', 'A')
('Q', 'K', 'A', 'J')
('Q', 'A', 'J', 'K')
('Q', 'A', 'K', 'J')
('K', 'J', 'Q', 'A')
('K', 'J', 'A', 'Q')
('K', 'Q', 'J', 'A')
('K', 'Q', 'A', 'J')
('K', 'A', 'J', 'Q')
('K', 'A', 'Q', 'J')
('A', 'J', 'Q', 'K')
('A', 'J', 'K', 'Q')
('A', 'Q', 'J', 'K')
('A', 'Q', 'K', 'J')
('A', 'K', 'J', 'Q')
('A', 'K', 'Q', 'J')
那么,一个均匀的洗牌算法,就是每次洗牌完后,获得上面每种顺序的概率是相等的,都等于1/24
。感觉已经出来了一种算法了,那就是先像前文所述把所有的排列情况都枚举出来,分别标上号 1-24 号,然后从 24 中随机取一个数字(先不考虑如何能做到随机取了,这个话题好像也没那么容易),获取其中这个数字对应的号的排列。
这个算法复杂度是多少?假设为 N
张牌的话,应该就是 1/N!
(注意是阶乘,这里可不是感叹号),显然复杂度太高了。
有没有更好的方法呢?答案当然是肯定的。
经典的洗牌算法
洗牌算法实际上是一个很经典的算法,在经典书籍《算法导论》里面很靠前的部分就有讲解和分析。
我们把这个洗牌过程用更加“程序员”的语言描述一下,就是假设有一个 n
个元素的数组 Array[n]
,通过某种方式,随机产生一个另外一个序列Array'[n]
让数组的每个元素 Array[i]
在数组中的每个位置出现的概率都是1/n
。
其实方法可以这样,依次从 Array
中随机选择 1 个,依次放到 Array'
中即可。证明一下:
Array[0]
,在新数组的第 0 个位置处的概率为:1/n
,因为随机选,只能是1/n
的概率能选中;Array[1]
,在新数组的第 1 个位置处的概率为:1/n
,因为 第一次没选中Array[1]
的概率为n-1/n
,再结合第二次(只剩下n-1个了,所以分母为n-1
)选中的概率为:1/n-1
,因此概率为: