耿老师教你学Java:交叉洗牌和Fisher-Yates洗牌(巧用队列)

交叉洗牌和Fisher-Yates洗牌

耿祥义

本文介绍两种洗牌算法:交叉洗牌和Fisher-Yates洗牌,并通过交叉洗牌警示我们:远离赌博(见代码运行效果)。交叉洗牌使得每张牌不在洗牌前的位置上(对于非单张),而Fisher-Yates 洗牌会偶尔发生某张牌未移动位置(尽管概率非常小);交叉洗牌没有随机性,除非随机排列初始牌,Fisher- Yates洗牌具有随机性

一、Fisher-Yates洗牌算法

n个数的不重复全排列有n!种可能,洗牌算法类似生活中打牌之前的洗牌。对n张牌进行洗牌后得到的结果是n!种排列中的某一个(是n!种排列之一的概率是相同的),不仅要使得每张牌都不在最初的位置上(对于非单张排),而且出现在其它每个位置上的概率也是相同的,这也正是洗牌算法的关键之处。

Fisher-Yates洗牌算法就是基本满足这样要求的洗牌算法(如果牌的数量较大每次洗牌后,某张牌没有挪动位置的概率非常小)。

Collections类将Fisher-Yates洗牌算法封装成

publicstaticvoid shuffle( List<?> list)

可以直接用类名 Collections调用该方法对list中的对象进行洗牌操作.

有关Fisher-Yates洗牌参见本公众号:

二、交叉洗牌

Fisher-Yates洗牌算法是伪随机洗牌(该算法里获取的随机数是伪随机数),生活中我们看到洗牌手拿着真实扑克牌洗牌是真实洗牌,只要他的方法(他可能有很多洗牌方法)被在场人信任或认可即可。这里我们使用队列模拟一种洗牌方法(我自己想的, 我不是洗牌手)。

(1)切牌

列card中有amount张牌(可以按1,2,3...升序排列也可以随机排列),然后,把牌分成分 firstHalf和secondHalf 两部分,二者中牌的数目相差一张,即如果amount是奇数,half =amount/2,否则half = amount/2+1 。

(2)交叉出列、入列

firstHalf和secondHalf交叉出列(注意是从各自的队列的尾部出列),然后重新入列card(从 card队尾入列):

card.offer(firstHalf.pollLast);

card.offer(secondHalf.pollLast);

进行(1),(2)就完成一次洗牌,反复进行 (1),(2)实现多次洗牌。

publicstaticvoidshuffleCard( ArrayDeque<Integer> card) { //交叉洗牌 intamount = card.size; //牌的数量 inthalf = 0; if(amount% 2== 0) half = amount/ 2+ 1; else half = amount/ 2; ArrayDeque<Integer> firstHalf = newArrayDeque<>; ArrayDeque<Integer> secondHalf = newArrayDeque<>; for( inti= 0;i<amount;i++){ //(1)切牌,分成二部分 if(i<half) firstHalf.offer(card.pop) ; else secondHalf.offer(card.pop); } while(!firstHalf.isEmpty||!secondHalf.isEmpty){ if(!firstHalf.isEmpty) //(2)交叉洗牌(交叉出列、入列) card.offer(firstHalf.pollLast); if(!secondHalf.isEmpty) card.offer(secondHalf.pollLast); } }

这里模拟的洗牌,每次洗牌后一定使得每张牌不在洗牌前的位置上(对于非单张),而Fisher-Yates洗牌会偶尔发生某张牌未移动位置(如果牌的数目较多,出现牌未移动位置的概率非常小),见程序运行效果,红色标记的牌。但是 这里的交叉洗牌缺少随机性:如果记住了初始牌和洗牌次数,就会知道每次的洗牌结果、特别是最后的洗牌结果(尽管结果似乎很随机),见后面出现运行效果( 所以,要远离赌博),否则无法预测洗牌结果。

三、代码与效果

多次运行效果,交叉洗牌的运行结果没有变化(没有随机性,除非初始牌有随机性),Fisher-Yates洗牌结果随机变化、有随机性。

1. 某次运行效果

2. 某次运行效果

三、代码

import java.util.*; publicclassXipai{ publicstaticvoidshuffleCard( ArrayDeque<Integer> card) { //交叉洗牌 intamount = card.size; //牌的数量 inthalf = 0; if(amount% 2== 0) half = amount/ 2+ 1; else half = amount/ 2; ArrayDeque<Integer> firstHalf = newArrayDeque<>; ArrayDeque<Integer> secondHalf = newArrayDeque<>; for( inti= 0;i<amount;i++){ //(1)切牌,分成二部分 if(i<half) firstHalf.offer(card.pop) ; else secondHalf.offer(card.pop); } while(!firstHalf.isEmpty||!secondHalf.isEmpty){ if(!firstHalf.isEmpty) //(2)交叉洗牌(交叉出列、入列) card.offer(firstHalf.pollLast); if(!secondHalf.isEmpty) card.offer(secondHalf.pollLast); } } publicstaticvoidmain( String args[]) { intcardAmount = 13, //牌的数量 count = 6; //洗牌次数 ArrayDeque<Integer> card = newArrayDeque<>; for( inti= 1;i<=cardAmount;i++) card. add(i); ArrayDeque<Integer> cardCopy = newArrayDeque<>(card); LinkedList<Integer> list= newLinkedList<>(card); LinkedList<Integer> listCopy= newLinkedList<>(list); System. out.println( "交叉洗牌"); System. out.println(card); intn = 0; while(count>= 0){ shuffleCard(card); //交叉洗牌 System. out.println(card); n += sameElements(card.iterator,cardCopy.iterator); //检查是否有牌在原位置 cardCopy = newArrayDeque<>(card); count--; } if(n!= 0){ System. out.println( "交叉洗牌过程中,出现某些牌的位置没有发生变化"); } count = 6; System. out.println( "经典Fisher-Yates洗牌"); System. out.println(list); n = 0; while(count>= 0){ Collections.shuffle(list); //Fisher-Yates洗牌 System. out.println(list); n += sameElements(list.iterator,listCopy.iterator); //检查是否有牌在原位置 listCopy = newLinkedList<>(list); count--; } if(n!= 0){ System. out.println( "Fisher-Yates洗牌过程中,出现某些牌的位置没有发生变化"); } } publicstaticintsameElements( Iterator<Integer> iterator1,Iterator<Integer> iterator2) { //检查牌的位置是否在原位置 intcount = 0; while(iterator1.hasNext){ if(iterator1.next==iterator2.next) count++; } returncount; } }

返回搜狐,查看更多

平台声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。
阅读 ()
我来说两句
0人参与, 0条评论
登录并发表