Python不用学,看看你就懂;拿来就能用,用用你就会
无需安装编程软件,把代码拷贝到在线编辑器即可运行
考虑一下扑克牌,如何用电脑编程做到高效而完美地洗牌呢?
要求是代码少、效率高,洗牌的结果要同时满足以下三个条件才算“完美”:初始状态无关性:洗牌后每张牌的位置与此前它的位置无关;
随机出现独立性:每张牌将出现的位置与其它牌是什么位置无关;
概率均衡相等性:每张牌在每个位置出现的概率完全一样,为1/N,N为牌的总张数。
你可以看到,那些“完美”的扑克牌手工洗牌——对半分然后一张一张交替插入的手法,虽然潇洒,洗牌效果却是不完美的,因为它显然不符合第1条“初始状态无关性”,并且也很有可能不符合第2条“随机出现独立性”。
还有一个隐含条件:不能重复,因为扑克牌每种牌只有一张。
考虑到上述条件,计算机编程应该如何做?
一种想法是,简单地采用范围 [1,N] 的随机数产生洗牌序列,每次与已经产生的牌点比较,有重复则放弃掉并重新产生。这种方法在已经产生大部分牌后,新生成的随机数与前面重复的可能性很大,因此要生成很多个随机数才能确定一张新位置的牌点。这样就不科学了,效率实在太低。
第二种想法是,随机产生第一张牌,然后用一个简单的算法依次产生后面的牌,算法本身保证牌点不会重复。为便于理解,例如只有5张牌,算法是循环加一个随机数d=[1,4]。若随机产生第一张牌3,且d=2,那么序列就是35241。若随机产生第一张牌4,且d=3,序列就是42531。表明上看这是双重随机,挺厉害的,其实此法不符合第2个条件独立随机性。所以这种做法虽然效率可以做到很高,但效果做不到完美。
第三种想法是,把扑克牌的所有排列组合保存下来,然后随机取一个。54张牌一共有54!种排列,存好,然后在[1,54!]范围中产生一个随机数,给出该随机数对应的那个序列即可。此法从洗牌结果来说,当然是完美的,但效率非常非常非常低下,老破计算机怕是要死机。^_^
以上都不行,那有没有完美而高效的洗牌算法呢?
有,比如大名鼎鼎的Knuth洗牌算法。其核心代码只有一个for循环:
for(int i = N - 1; i >= 0 ; i -- )
swap(arr[i], arr[rand(0, i)]) // rand(0, i) 生成 [0, i] 之间的随机整数
这个算法非常巧妙,被誉为“神一样的算法”,然而解释它是要费点儿功夫的,还要搞些图片甚至动画什么的来帮助理解,例如:有哪些算法惊艳到了你?你若觉得理解这个算法有点儿费脑筋费时间,没关系,请先看看我下面给的算法。
我们也可以把洗牌看作是一个随机发完牌的过程:从原牌堆中一张一张随机取出牌,依次放到新牌堆中(每取出一张,原牌堆中就要删除这张),取完了,新牌堆就是一副洗好的牌。
这种方法直观而容易理解。它其实是Knuth洗牌算法的前身,叫Fisher-Yates洗牌算法
for i in range(N):
list2.append(random.choice(list1)) # 从原牌堆中随机抽取一张牌放到新牌堆中
list1.remove(list2[i]) # 从原牌堆中删除刚才抽到的那张牌
怎么样?很好理解吧。理解了这个算法,再点进去看刚才提到的帖子,你会发现Knuth算法相当于只是省掉了第二个数组的空间,把第一个数组空出来的位置给新牌堆用而已。所以这样一来,Knuth算法就变得很好理解了。
根据上述过程描述,该算法显然满足完美性的第1个条件和第2个条件,而要证明其满足第3个条件“等概率性”,也很简单:总牌数为N,则任意一张牌出现在第1个位置的几率为1/N,出现在第2个位置的几率为(1-1/N) x 1/(N-1) = 1/N(即第1个位置没出现而现在恰好出现的几率),余此类推,出现在任意第k个位置的几率也是1/N:
此算法的效率虽然比Knuth洗牌算法低一些,但也相当不错,实测在普通家用电脑上54张牌洗一万次,或者一万张牌洗一次,都是瞬间完成的事情。关键是,此算法无论其原理还是其Python编程的实现,都很好理解,所以我们把它作为高效而完美洗牌的Python编程范例。
下面是采用Python编程的完整代码及演算结果示例:
import random
TOTAL_NUMBER = 54 # 牌的总张数
DEAL_NUMBER = 54 # 发牌张数,等于牌的总张数时相当于洗牌
list1 = [i+1 for i in range(TOTAL_NUMBER)] # 初始化原牌堆,方法无所谓,完整而不重复即可
list2 = [] # 新牌堆开始时为空
for i in range(DEAL_NUMBER):
list2.append(random.choice(list1)) # 从原牌堆中随机抽取一张牌放到新牌堆中
list1.remove(list2[i]) # 从原牌堆中删除刚才抽到的那张牌
print("A new card list produced:\n",list2)
---------------------
A new card list produced:
[52, 54, 15, 47, 14, 36, 19, 43, 8, 12, 30, 34, 18, 27, 16, 39, 40, 24, 2, 50, 10, 51, 7, 21, 42, 6, 45, 31, 25, 28, 5, 4, 1, 35, 46, 17, 13, 29, 48, 32, 11, 9, 33, 37, 20, 38, 44, 53, 23, 41, 22, 26, 3, 49]
Program ended with exit code: 0
这段程序同时也是一个简单高效的发牌程序哦,把DEAL_NUMBER设为一个你需要的、且比TOTAL_NUMBER小的值就可以了,比如把DEAL_NUMBER设为7,那就是一次发7张牌:
A new card list produced:
[8, 32, 12, 20, 33, 45, 25]
Program ended with exit code: 0
如果,你觉得代码还想短一点,Python还逆天地直接提供洗牌函数random.shuffle(list),一句话搞定。可怕吗?就像这样:
random.shuffle(list1)
print("Shuffle directly by random.shuffle function:\n",list1)
-----------
Shuffle directly by random.shuffle function:
[43, 36, 15, 50, 26, 9, 48, 10, 25, 20, 33, 29, 49, 52, 5, 19, 37, 39, 14, 21, 34, 45, 40, 24, 12, 27, 23, 16, 38, 4, 22, 51, 17, 46, 47, 53, 8, 41, 54, 18, 28, 32, 7, 42, 35, 6, 31]
这里是《简单又好玩的Python》专栏,欢迎关注。
源代码文件地址:
参考