交叉洗牌和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;
}
}
![]()
![]()
![]()
返回搜狐,查看更多