几乎所有的程序员都写过类似于“洗牌”的算法,也就是将一个数组随机打乱后输出,虽然很简单,但是深入研究起来,这个小小的算法也是大有讲究。我在面试程序员的时候,就会经常让他们当场写一个洗牌的函数,从中可以观察到他们对于这个问题的理解和写程序的基本功。
在深入讨论之前,必须先定义出一个基本概念:究竟洗牌算法的本质是什么?也就是说,什么样的洗牌结果是“正确”的?
云风曾经有一篇博文,专门讨论了这个问题,他也给出了一个比较确切的定义,在经过洗牌函数后,如果能够保证每一个数据出现在所有位置的概率是相等的,那么这种算法是符合要求的。在这个前提下,尽量降低时间复杂度和空间复杂度就能得到好的算法。
第一个洗牌算法:
随机抽出一张牌,检查这张牌是否被抽取过,如果已经被抽取过,则重新抽取,直到找到没被抽出过的牌,然后把这张牌放入洗好的队列中,重复该过程,直到所有的牌被抽出。
大概是比较符合大脑对于洗牌的直观思维,这个算法经常出现在我遇到的面试结果中,虽然它符合我们对于洗牌算法的基本要求,但这个算法并不好,首先它的复杂度为O(N2),而且需要额外的内存空间保存已经被抽出的牌的索引。所以当数据量比较大时,会极大降低效率。
第二个算法:
设牌的张数为n,首先准备n个不容易碰撞的随机数,然后进行排序,通过排序可以得到一个打乱次序的序列,按照这个序列将牌打乱。
这也是一个符合要求的算法,但是同样需要额外的存储空间,在复杂度上也会取决于所采用的排序算法,所以仍然不是一个好的算法。
第三个算法:
每次随机抽出两张牌交换,重复交换一定次数次后结束
void shuffle(int data, int length)
{
for(int i=0; i<SWAP_COUNTS; i++)
{
//Rand(min, max)返回[min, max)区间内的随机数
int index1 = Rand(0, length);
int index2 = Rand(0, length);
std::swap(data[index1], data[index2]);
}
}
这又是一个常见的洗牌方法,比较有意思的问题是其中的“交换次数”,我们该如何确定一个合适的交换次数?简单的计算,交换m次后,具体某张牌始终没有被抽到的概率为((n-2)/n)^m,如果我们要求这个概率小于1/1000,那么 m>-3ln(10)/ln(1-2/n),对于52张牌,这个数大约是176次,需要注意的是,这是满足“具体某张牌”始终没有被抽到的概率,如果需要满足“任意一张牌”没被抽到的概率小于1/1000,需要的次数还要大一些,但这个概率计算起来比较复杂,有兴趣的朋友可以试一下。
Update: 这个概率是,推算过程可以参考这里,根据这个概率,需要交换280次才能符合要求
第四个算法:
从第一张牌开始,将每张牌和随机的一张牌进行交换
void shuffle(int data, int length)
{
for(int i=0; i<length; i++)
{
int index = Rand(0, length);
std::swap(data[i], data[index]);
}
}
很明显,这个算法是符合我们先前的要求的,时间复杂度为O(N),而且也不需要额外的临时空间,似乎我们找到了最优的算法,然而事实并非如此,看下一个算法。
第五个算法:
void shuffle(int data, int length)
{
for(int i=1; i<length; i++)
{
int index = Rand(0, i);
std::swap(data[i], data[index]);
}
}
一个有意思的情况出现了,这个算法和第三种算法非常相似,从直觉来说,似乎使数据“杂乱”的能力还要弱于第三种,但事实上,这种算法要强于第三种。要想严格的证明这一点并不容易,需要一些数学功底,有兴趣的朋友可以参照一下这篇论文,或者matrix67大牛的博文,也可以这样简单理解一下,对于n张牌的数据,实际排列的可能情况为n! 种,但第四种算法能够产生n^n种排列,远远大于实际的排列情况,而且n^n不能被n!整除,所以经过算法四所定义的牌与牌之间的交换程序,很可能一张牌被换来换去又被换回到原来的位置,所以这个算法不是最优的。而算法五输出的可能组合恰好是n!种,所以这个算法才是完美的。
事情并没有结束,如果真的要找一个最优的算法,还是请出最终的冠军吧!
第六个算法:
void shuffle(int data, int length)
{
std::random_shuffle(data, data+length);
}
没错,用c++的标准库函数才是最优方案,事实上,std::random_shuffle在实现上也是采取了第四种方法,看来还是那句话,“不要重复制造轮子”
不想写 - -
分类: 电脑/网络 >> 程序设计 >> 其他编程语言
解析:
用随机函数来模拟比较合适
扑克牌有54张,可以先约定好,比如:
大猫----------0
小猫----------1
黑桃A---------1
黑桃2---------黑桃A + 1
黑桃3---------黑桃A + 2
黑桃K---------黑桃A + 12
红心A---------黑桃A + 13
红心2---------红心A + 1
这样就可以把整副扑克牌的每张牌用一个特定的整数来表示,它们之间的大小关系可以通过不同的玩法制定相应的规则。
那好,现在我们定义一副牌int Joker[54];
用随机函数来填充Joker。
算法为:(不是真实的程序语言,由于不知道你使用什么编程语言,这里只描述算法)
for(i = 0 to 53)
{
l1:
生成随机数:pai = rand(54); 随机生成一个0 -- 53的整数
在已产生的牌中查找是否存在pai(即在Joker[0] 到Joker[i - 1]中查找)
如果存在,goto l1;
否则Joker[i] = pai
}
这样我们就把整副牌给洗好了。
接下来,就可以发牌了。
发牌时,只要按顺序把牌“发”到每个人的“手”里就行了
例如:4个人玩牌,四个人的牌为player[4][14];
for(i = 0 to 53)
{
player[i % 4][i / 4] = Joker[i];
}
这样就完成了发牌程序。
当然,如果你想要发牌也用一维数组,那也简单,比如还是4个人玩,那么:
player0手上的第n张牌就是Joker[n 4 + 0]
player1手上的第n张牌就是Joker[n 4 + 1]
player2手上的第n张牌就是Joker[n 4 + 2]
player3手上的第n张牌就是Joker[n 4 + 3]
好了,洗牌和发牌都已经完成,剩下就是如何玩牌了,你没问,就不多说了。
上面的伪代码应该能看懂吧?如果有问题,就用消息联系好了。你自己用编程语言去实现,不是什么大问题的。
全自动麻将机利用两副牌交替洗牌,使得打麻将如行云流水般进行,大大加快了打麻将的节奏,深受大家喜爱。不过使用麻将机时会出现一些小问题。这里小编为大家介绍当不小心没有把牌全放入机器中,机器会因为这副牌少了几个一直进行洗牌状态,而且无法打开开口时的处理办法。
1出现这种情况处理办法很简单,可能因为有些朋友不知道就是束手无策。
2控制台面升降的有两个按钮,平时正常情况下按一个就可以了。
3当出现上述故障时,同时按下两个升降按钮,麻将机就可以执行强制打开台面的命令了。
4把没装进去的牌放进去,重新洗牌就好了。
大盘内总是有一颗牌洗不来,导致机器报警,原因主要有:①麻将牌有问题,②吸牌轮磁铁磁性有问题,③大盘布翘起影响上牌,④牛筋断。
大盘内经常出现有二张或三张牌洗不上来,甚至两张牌粘在一起,此时要:①检查翻牌磁铁的位置,②刮牌条的位置,如都没有问题,将大盘其中一根刮牌条变短一点,再把大盘的两个长的运牌片变短(大盘一圈八个黑色的)同时在洗牌盘上加装一个牛筋钉(方形的塑料块)请您根据自身实际情况谨慎 *** 作。尤其涉及您或第三方利益等事项,请咨询专业人士处理。
——补上一篇文里本该有的图解,不知道为啥手有点浮肿拍的有点奇怪
准备一副新牌
从里面随便抽出来一摞
抽出来的这一摞叠到最上方,反复多次,直到洗匀
一副牌,习惯性正位对自己吧,其实麻将洗完都一样了,这个无所谓
用一只手的拇指下方处压住牌摞尾部
以手掌为中心先转成一个圆圈,然后用两只手按住所有的牌,使其一边以单手为中心自转一边以上图第一次旋转的圆心为轴心周转一手还要拿手机的孤儿拍不出来双手图解
整理好后随便找一面对着自己,在麻将洗完全洗匀的情况下可以当做这个几率是平均分的
分成若干牌组,S型发牌,把所有牌都发进去
整理后用扑克洗再洗几遍确保洗匀,也可以整理后多次重复发牌
一般来说切牌切为三份牌,最上面的三分之一成为第二组,中间的三分之一成为第三组,最下面的三分之一保留在原地。然后按从左到右或者从右到左的规律顺序整理起来
其实跟扑克洗是一样的,为了让你从提问状态理清思绪平静下来,也为了给没洗匀的手残同学一个最后保障,其实不是必要的
/洗牌程序,控制台程序,没有,可自行去改成有图的窗口程序/
using System;
using SystemCollectionsGeneric;
using SystemLinq;
using SystemText;
namespace ConsoleApp2
{
class Card
{
string []card= { "红桃A", "红桃2", "红桃3", "红桃4", "红桃5", "红桃6", "红桃7", "红桃8", "红桃9", "红桃10", "红桃J", "红桃Q", "红桃K",
"黑桃A", "黑桃2", "黑桃3", "黑桃4", "黑桃5", "黑桃6", "黑桃7", "黑桃8", "黑桃9", "黑桃10", "黑桃J", "黑桃Q", "黑桃K",
"方块A", "方块2", "方块3", "方块4", "方块5", "方块6", "方块7", "方块8", "方块9", "方块10", "方块J", "方块Q", "方块K",
"梅花A", "梅花2", "梅花3", "梅花4", "梅花5", "梅花6", "梅花7", "梅花8", "梅花9", "梅花10", "梅花J", "梅花Q", "梅花K" };
string[] deck;
public Card()
{
ConsoleWriteLine("初始值: ");
for (int i = 0; i < 52; i++)
{
ConsoleWrite(card[i]);
if ((i + 1) % 13 != 0) ConsoleWrite(" ");
else ConsoleWriteLine();
}
}
void start()
{
ConsoleWriteLine("洗牌:");
Random t = new Random();
int []a= new int[52];
deck = new string[52];
for (int i = 0; i < 52; i++)
{
int tmp = tNext(0, 52);
a[i] = tmp;
bool bl = true;
for(int j = 0; j < i;j++)
{
if (a[j] == tmp)
{
bl = false;
--i;
break;
}
}
if (bl==true)
{
deck[i] = card[a[i]];
ConsoleWrite(deck[i]);
if ((i + 1) % 13 != 0) ConsoleWrite(" ");
else ConsoleWriteLine();
}
}
}
void send(string s,int n)
{
ConsoleWriteLine(s);
for (int i = 0; i <13; i++)
{
ConsoleWrite(deck[i 4 + n]);
if ((i + 1) % 13 != 0) ConsoleWrite(" ");
else ConsoleWriteLine();
}
}
void sender()
{
start();
send("甲的牌:", 0);
send("乙的牌:", 1);
send("丙的牌:", 2);
send("丁的牌:", 3);
}
static void Main(string[] args)
{
Card mma = new Card();
mmasender();
ConsoleReadKey();
}
}
}
以上就是关于用C++编写一个洗牌发牌的函数,玩家可能有两个、三个和四个全部的内容,包括:用C++编写一个洗牌发牌的函数,玩家可能有两个、三个和四个、如何用一维数组模仿扑克洗牌和发牌、全自动麻将机怎样洗牌等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)