如何测试洗牌程序
我希望本文有助于你了解测试软件是一件很重要也是一件不简单的事。
我们有一个程序,叫ShuffleArray(),是用来洗牌的,我见过N多千变万化的ShuffleArray(),但是似乎从来没人去想过怎么去测试这个算法。所以,我在面试中我经常会问应聘者如何测试ShuffleArray(),没想到这个问题居然难倒了很多有多年编程经验的人。对于这类的问题,其实,测试程序可能比算法更难写,代码更多。而这个问题正好可以加强一下我在《我们需要专职的QA吗?》中我所推崇的——开发人员更适合做测试的观点。
我们先来看几个算法(第一个用递归二分随机抽牌,第二个比较偷机取巧,第三个比较通俗易懂)
目录
递归二分随机抽牌
有一次是有一个朋友做了一个网页版的扑克游戏,他用到的算法就是想模拟平时我们玩牌时用手洗牌的方式,是用递归+二分法,我说这个程序恐怕不对吧。他觉得挺对的,说测试了没有问题。他的程序大致如下(原来的是用Javascript写的,我在这里凭记忆用C复现一下):
//递归二分方法 const size_t MAXLEN = 10; const char TestArr[MAXLEN] = {'A','B','C','D','E','F','G','H','I','J'}; static char RecurArr[MAXLEN]={0}; static int cnt = 0; void ShuffleArray_Recursive_Tmp(char* arr, int len) { if(cnt > MAXLEN || len <=0){ return; } int pos = rand() % len; RecurArr[cnt++] = arr[pos]; if (len==1) return; ShuffleArray_Recursive_Tmp(arr, pos); ShuffleArray_Recursive_Tmp(arr+pos+1, len-pos-1); } void ShuffleArray_Recursive(char* arr, int len) { memset(RecurArr, 0, sizeof(RecurArr)); cnt=0; ShuffleArray_Recursive_Tmp(arr, len); memcpy(arr, RecurArr, len); } void main() { char temp[MAXLEN]={0}; for(int i=0; i<5; i++) { strncpy(temp, TestArr, MAXLEN); ShuffleArray_Recursive((char*)temp, MAXLEN); } }
随便测试几次,还真像那么回事:
第一次:D C A B H E G F I J 第二次:A G D B C E F J H I 第三次:A B H F C E D G I J 第四次:J I F B A D C E H G 第五次:F B A D C E H G I J
快排Hack法
让我们再看一个hack 快排的洗牌程序(只看算法,省去别的代码):
int compare( const void *a, const void *b ) { return rand()%3-1; } void ShuffleArray_Sort(char* arr, int len) { qsort( (void *)arr, (size_t)len, sizeof(char), compare ); }
运行个几次,感觉得还像那么回事:
第一次:H C D J F E A G B I 第二次:B F J D C E I H G A 第三次:C G D E J F B I A H 第四次:H C B J D F G E I A 第五次:D B C F E A I H G J
看不出有什么破绽。
大多数人的实现
下面这个算法是大多数人的实现,就是for循环一次,然后随机交换两个数
void ShuffleArray_General(char* arr, int len) { const int suff_time = len; for(int idx=0; idx<suff_time; idx++) { int i = rand() % len; int j = rand() % len; char temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
跑起来也还不错,洗得挺好的。
第一次:G F C D A J B I H E 第二次:D G J F E I A H C B 第三次:C J E F A D G B H I 第四次:H D C F A E B J I G 第五次:E A J F B I H G D C
但是上述三个算法哪个的效果更好?好像都是对的。一般的QA或是程序员很有可能就这样把这个功能Pass了。但是事情并没有那么简单……
如何测试
在做测试之前,我们还需要了解一下一个基本知识——PC机上是做不出真随机数的,只能做出伪随机数。真随机数需要硬件支持。但是不是这样我们就无法测试了呢,不是的。我们依然可以测试。
我们知道,洗牌洗得好不好,主要是看是不是够随机。那么如何测试随机性呢?
试想,我们有个随机函数rand()返回1到10中的一个数,如果够随机的话,每个数返回的概率都应该是一样的,也就是说每个数都应该有10分之1的概率会被返回。
一到概率问题,我们只有一个方法来做测试,那就是用统计的方式。也就是说,你调用rand()函数100次,其中,每个数出现的次数大约都在10次左右。(注意:我用了左右,这说明概率并不是很准确的)不应该有一个数出现了15次以上,另一个在5次以下,要是这样的话,这个函数就是错的。
举一反三,测试洗牌程序也一样,需要通过概率的方式来做统计,是不是每张牌出现在第一个位置的次数都是差不多的。
于是,这样一来上面的程序就可以很容易做测试了。
下面是测试结果(测试样本1000次——列是每个位置出现的次数,行是各个字符的统计,出现概率应该是1/10,也就是100次):
递归随机抽牌的方法
很明显,这个洗牌程序太有问题。算法是错的!
1 2 3 4 5 6 7 8 9 10 ---------------------------------------------------- A | 101 283 317 208 65 23 3 0 0 0 B | 101 191 273 239 127 54 12 2 1 0 C | 103 167 141 204 229 115 32 7 2 0 D | 103 103 87 128 242 195 112 26 3 1 E | 104 83 62 67 116 222 228 93 22 3 F | 91 58 34 60 69 141 234 241 65 7 G | 93 43 35 19 44 102 174 274 185 31 H | 94 28 27 27 46 68 94 173 310 133 I | 119 27 11 30 28 49 64 96 262 314 J | 91 17 13 18 34 31 47 88 150 511
快排Hack法
看看对角线(从左上到右下)上的数据,很离谱!所以,这个算法也是错的。
1 2 3 4 5 6 7 8 9 10 ----------------------------------------------------- A | 74 108 123 102 93 198 40 37 52 173 B | 261 170 114 70 49 28 37 76 116 79 C | 112 164 168 117 71 37 62 96 116 57 D | 93 91 119 221 103 66 91 98 78 40 E | 62 60 82 90 290 112 95 98 71 40 F | 46 60 63 76 81 318 56 42 70 188 G | 72 57 68 77 83 39 400 105 55 44 H | 99 79 70 73 87 34 124 317 78 39 I | 127 112 102 90 81 24 57 83 248 76 J | 54 99 91 84 62 144 38 48 116 264
大多数人的算法
我们再来看看大多数人的算法。还是对角线上的数据有问题,所以,还是错的。
1 2 3 4 5 6 7 8 9 10 ----------------------------------------------------- A | 178 98 92 82 101 85 79 105 87 93 B | 88 205 90 94 77 84 93 86 106 77 C | 93 99 185 96 83 87 98 88 82 89 D | 105 85 89 190 92 94 105 73 80 87 E | 97 74 85 88 204 91 80 90 100 91 F | 85 84 90 91 96 178 90 91 105 90 G | 81 84 84 104 102 105 197 75 79 89 H | 84 99 107 86 82 78 92 205 79 88 I | 102 72 88 94 87 103 94 92 187 81 J | 87 100 90 75 76 95 72 95 95 215
正确的算法
下面,我们来看看性能高且正确的算法—— Fisher_Yates算法
void ShuffleArray_Fisher_Yates(char* arr, int len) { int i = len, j; char temp; if ( i == 0 ) return; while ( --i ) { j = rand() % (i+1); temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
这个算法不难理解,看看测试效果(效果明显比前面的要好):
1 2 3 4 5 6 7 8 9 10 ----------------------------------------------------- A | 107 98 83 115 89 103 105 99 94 107 B | 91 106 90 102 88 100 102 97 112 112 C | 100 107 99 108 101 99 86 99 101 100 D | 96 85 108 101 117 103 102 96 108 84 E | 106 89 102 86 88 107 114 109 100 99 F | 109 96 87 94 98 102 109 101 92 102 G | 94 95 119 110 97 112 89 101 89 94 H | 93 102 102 103 100 89 107 105 101 98 I | 99 110 111 101 102 79 103 89 104 102 J | 105 112 99 99 108 106 95 95 99 82
但是我们可以看到还是不完美。因为我们使用的rand()是伪随机数,不过已经很不错的。最大的误差在20%左右。
我们再来看看洗牌100万次的统计值,你会看到误差在6%以内了。这个对于伪随机数生成的程序已经很不错了。
1 2 3 4 5 6 7 8 9 10 ------------------------------------------------------------------------- A | 100095 99939 100451 99647 99321 100189 100284 99565 100525 99984 B | 99659 100394 99699 100436 99989 100401 99502 100125 100082 99713 C | 99938 99978 100384 100413 100045 99866 99945 100025 99388 100018 D | 99972 99954 99751 100112 100503 99461 99932 99881 100223 100211 E | 100041 100086 99966 99441 100401 99958 99997 100159 99884 100067 F | 100491 100294 100164 100321 99902 99819 99449 100130 99623 99807 G | 99822 99636 99924 100172 99738 100567 100427 99871 100125 99718 H | 99445 100328 99720 99922 100075 99804 100127 99851 100526 100202 I | 100269 100001 99542 99835 100070 99894 100229 100181 99718 100261 J | 100268 99390 100399 99701 99956 100041 100108 100212 99906 100019
如何写测试案例
测试程序其实很容易写了。就是,设置一个样本大小,做一下统计,然后计算一下误差值是否在可以容忍的范围内。比如:
- 样本:100万次
- 最大误差:10%以内
- 平均误差:5%以内 (或者:90%以上的误差要小于5%)
注意
其实,以上的测试只是测试了牌在各个位置的概率。这个还不足够好。因为还可能会现在有Patten的情况。如:每次洗牌出来的都是一个循环顺序数组。这完全可以满足我上面的测试条件。但是那明显是错的。所以,还需要统计每种排列的出现的次数,看看是不是均匀。但是,如果这些排列又是以某种规律出现的呢?看来,这没完没了了。
测试的确是一个很重要,并不简单的事情。谢谢所有参与讨论的人。
附录
之前忘贴了一个模拟我们玩牌洗牌的算法,现补充如下:
void ShuffleArray_Manual(char* arr, int len) { int mid = len / 2; for (int n=0; n<5; n++){ //两手洗牌 for (int i=1; i<mid; i+=2){ char tmp = arr[i]; arr[i] = arr[mid+i]; arr[mid+i] = tmp; } //随机切牌 char *buf = (char*)malloc(sizeof(char)*len); for(int j=0; j<5; j++) { int start= rand() % (len-1) + 1; int numCards= rand()% (len/2) + 1; if (start + numCards > len ){ numCards = len - start; } memset(buf, 0, len); strncpy(buf, arr, start); strncpy(arr, arr+start, numCards); strncpy(arr+numCards, buf, start); } free(buf); } }
我们来看看测试结果:(10万次)效果更好一些,误差在2%以内了。
1 2 3 4 5 6 7 8 9 10 ------------------------------------------------------------------------- A | 10002 9998 9924 10006 10048 10200 9939 9812 10080 9991 B | 9939 9962 10118 10007 9974 10037 10149 10052 9761 10001 C | 10054 10100 10050 9961 9856 9996 9853 10016 9928 10186 D | 9851 9939 9852 10076 10208 10003 9974 10052 9992 10053 E | 10009 9915 10050 10037 9923 10094 10078 10059 9880 9955 F | 10151 10115 10113 9919 9844 9896 9891 9904 10225 9942 G | 10001 10116 10097 10030 10061 9993 9891 9922 9889 10000 H | 10075 10033 9866 9857 10170 9854 10062 10078 10056 9949 I | 10045 9864 9879 10066 9930 9919 10085 10104 10095 10013 J | 9873 9958 10051 10041 9986 10008 10078 10001 10094 9910
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《如何测试洗牌程序》的相关评论
用统计频率来测试随机性,也太随意了吧……
绝对不应该这么测试的,有专门的随机测试办法的
”(注意:我用了左右,这说明概率并不是很准确的)“又是在说什么
概率是准确的精确值,但是频率依据大数定理的证明,只是在样本数趋无穷的情况下趋近概率,当然是不准确的……
你说说怎么测试?我也学习学习。
双手洗牌是这样洗的吗……?是我平时用的不是这种洗法抑或我没看懂代码啊……想不到那段代码模拟的双手洗牌在现实中怎么操作的……
平分,交叉穿插。
我也没看懂那个两手洗牌,没看明白怎么洗的@这是芦苇
楼主是否听说过 ent(http://fourmilab.ch/random)?
@陈皓
搞出第N大的排列不难. 难的是如何均匀地从[1, n!]中选取N. n为位数, 而且可能比较大.
嗯,的确是,第N的排列的公式其实努力想想还是想得出来的。
哈哈~没想到老祖宗留下来的看似随意的方法原来这么好用!!
记得编程珠玑上也有排列算法的.不知道是哪个?!@
看了下 Python中的random.py的代码好shuffle用的就是Fisher_Yates算法 └(^o^)┘
恩,小时候物理老师教我们怎么做实验的原理也是这样的
正好我也刚刚关注过这个问题: http://bbs.csdn.net/topics/390255889
不过关注点是:rand(n)还是rand(i)
没找到更好的验证机制
rand(i)是对应了n!的组合可能,但是rand(n)从机制上说,也没有偏袒任何数字。。。。
昨天看了微博就知道皓哥一定会些博客来得瑟,果然今天就看到了。皓哥这里提到的测试和一般所谓的软件验证测试本来就不是一个概念。一般的测试只是验证软件的结果和需求是否匹配,一般叫黑盒。像洗牌的你要先给出需求比如多次洗牌出现同模式的必须小于多少或者各个位置的需求作为输入.模糊的说洗牌程序那就只有写程序的人知道是做什么的。 白盒的测试也只是检验代码实现的逻辑和设计的逻辑是否匹配。有了预先的设计,才能验证。 也就是说一般讨论的测试就是如何验证软件结果与预期是否一致。而不是有多好。算法的好坏自己然开发做的。
就算拿你列举的前三份代码来说, 他们的代码实现首先就没有验证。如果代码中本身就有错误呢?比如,扑克牌是54张的。测试代码里只用了10个数,伪随机序列只有在长度比较大的情况下,数值才能体现随机性。想你列举的测试过程,应该每次改变随机因子。代码本身使用随机函数方法就有问题。
我相信,不是大部分资深程序员回答不了你的问题,而是你的问题本来就有问题。洗牌程序怎么测? 鬼知道怎么测。“测”的前提就是要了解需求,根据需求细化case。但是一个需求可能覆盖到的软件路径是极多的,所以需要有一定的方法。当然现在没有完美的方法,但是出去工程话的管理,就要有总结方法去控制风险。这个敏捷的目的是一样的。我不是推崇敏捷,只是当软件作为工程概念的时候,需要科学的方法管理。尽管未必尽善尽美,但也要尽可可控。而不是像艺术家一样的天马行空。 当然国内很多人水平有限,这就给了皓哥攻击的砝码。
皓哥每次都是变着法的偷换概念,拿着国外大牛狐假虎威。大师们自然有自己的门道,但是作为一个行业来说,软件开发和测试都是在不断探索科学化的管理方法的。这才有可能想现在大家都可以写出可以用的软件。不然,软件就只能停留在只有大师们和皓哥你这样的人才能写软件的阶段了。 当然已皓哥的睿智自然是明白这些的,这都只不过是作为技术人员的一点得瑟之心而已。会写点代码的人,总是瞧不起别人,把自己当大牛,把自己的准则当作准神,其实狭隘的一塌糊涂。
皓哥所有的文章都不过是为了YY自己的技术技高一筹,已产生一种自己比肩大师的自我满足,根本无所谓所讨论问题的定义究竟是什么。踩着一群国内大忽悠的身体,被众人的吹捧送上神坛。 微博里最后还让别人来出题,就为的自己show一把。
浏览了一下皓哥的博客,所有文章里,有营养的文章有两类 1)转贴大师的话+comments 2)转贴 还大言不惭抨击infoQ的空洞文章。
补充一点,皓哥当年的Makefile对于年幼无知的我还算是上品。
当然皓哥会把我打为敏捷控,人身攻击控,空洞理论控等牛鬼蛇神直接枪毙。然后封我ip,禁我评论。于是又是一片赞叹,万众景仰,顶礼膜拜。
说伪随取模不够随机的… RAND_MAX通常是多少? 扑克牌有多少张? 那么这个问题的实际影响有多大? 对于这个测试影响有多大?
“试想,我们有个随机函数rand()返回1到10中的一个数,如果够随机的话,每个数返回的概率都应该是一样的,也就是说每个数都应该有10分之1的概率会被返回。” 这种说法是错的, 一个硬币扔两次就一定一正一反吗? 真懂概率的人, 真不多…
楼上的你不是敏捷控是什么? 不过我觉得设计师跟包工头打架这事儿没啥意思.
那个叫M的该是个什么样的SB啊
我测试了一下,至少结果确实不是我平时双手洗牌的效果,我双手洗牌的时候事实上是将一半的牌组A的牌逐张插入到另一半的牌组B的两张牌之间,在新的整副牌中,牌组A的牌的相对位置还是没有变的,牌组B的牌的相对位置也没有变。
也就是说原本a,b.c.d.e.f.g,洗了之后应该是a,e,b,f,c,g,d.难道不是这样双手洗的吗?@zhangzhenyue
真是不错的文章,开眼界了。 另,楼上那个M是疯狗吗?
这个人的网名叫michaelchen,自从因为博主批评敏捷的事,他就开始骂博主了,说话阴阳乱气的,博主基本都不理他。我在博主的微博和图灵的采访那边也看到过他的SB评论。
我觉得他是爱上博主了,因为博主出现的地方都有他的身影,因为博主已经成了他生活中不可缺少的一部分了。
我是专门来赞老师的
@colin
我同意你的说法,
这文章的测试方法比较原始……我到现在都不懂他为啥要测这个所谓的“误差”以及这个“误差”能“证明”啥。
在american scientist的2011年7,8月刊有篇文章讲到随机数的叫Quasirandom Ramblings,里面提到(几乎所有关于概率的教材都会提到)随机数必须符合:1.不可以预测,2,相互独立,3,均匀分布。
我想如果文章的测试程序也仅仅是测试了均匀分布,而没有测试“不可以预测”,“相互独立”。而且像这种所谓的测试根本就不可能去“证明算法”是对还是错的。仅仅是“测试”了均匀分布就算是测试了?这有点鬼扯吧!如果你需要完备的测试的话,我觉得2 gram, 3 gram估计都不够,而且这“够不够”也完全是很人为的设定的,就跟语义识别的做法一样。大部分伪随机数都仅仅是符合了均匀分布而已。
开发前就没有说清楚对程序的需求,后面又要求测试能够“完备”,然后又责备测试人员不懂测试这个程序,最后还说测试人员肯定会就这样pass了。拜托博主,我也是测试人员,也没有你说的那么肤浅,肤浅到连概率统计都不懂。而且你这样“情绪化”的下这个结论,也暴露出你的逻辑修养不足–不是就事论事,而是以自己的偏见论事。
如果你真的要测试随机性,还不如用个程序去捕获硬件的电子噪声,这个白噪声完全是随机的,现在有些Unix系统的原生random程序就是以这个做出来的。
M 你好,
你说的虽有道理,但言语过于犀利。毕竟大家的目地是为了交流和学习技术。我觉的这里的很多文章还是很有价值的,尤其提供了一个中文环境下的技术学习窗口和沟通渠道。听你的话好像有很多其他的见解,如果有个人博客/网站的话,是否介意也公开出来和大家分享?
我们来看一下第三个方法(大多数人算法)为啥对角线会有问题。每次洗牌循环交换10次,洗牌结束后,A排在第一位有两种情况,一种是这10次交换没有交换到A,另一种是第10次交换将其他位置的A交换到第一位。前一种的概率是0.8^10;后一种的概率大概为(前9次交换交换到A的概率)*(1/9),约等于0.1;加一起大约0.2。很明显这算法本身有问题,和伪随机真随机没有什么关系。而且我们可以看到这个误差和数组长度有关(虽然实际上影响不大)。用10个元素的数组测试顶多能验证算法本身有没有系统性错误,而不能真正检验实际情况下(54张牌)的误差分布。
测试案例那一项有=一种情况没考虑到:
如果测试100w次,得出的结果为每个字符10W次,或者相差很少,10以内,算不算是缺陷。
floyd随机算法
额,数学上说洗牌次数为7次时最为混乱。。。。
其实“开发人员更适合做测试”这句话给我的理解是——测试人员应该要比开发更懂这个软件的业务和代码,否则就不会被开发忽悠。。。
@茶话汇
我的观点是:开发人员不适合测试自己的代码,写自己项目的文档,因为人往往熟视无睹,自己知道了以为别人也知道
测试尤其危险,因为开发人员往往走正确的操作路径,也就很难出错了
谢谢。我没有什么技术博客,我也不是搞技术的。我也没什么技术。早期学习makefile的时候拜读了皓哥的跟我一起些makefile 感觉非常不错。后来就关注了皓哥的博客。看到他对敏捷的一些观点颇为偏颇。偷换概念,根本没说到点子,所以发表了一些观点。想不到皓哥不就具体问题反驳,就把我打入了“疯狗派”,还禁我评论。所以我看了其他的文章,才发现了皓哥所谓技术文章的奥义,就不过是技术人员特有的自我感觉良好。凡是会写点代码的人都有这种情节。看不起测试,看不起windows,经不得批评。拿一些大牛当神拜,等等。 很多人不动脑子满口脏话,还说我骂人。我所发表的言论如有骂人之处,我愿意奉献2000w给被骂的人。 @lailai23
完全同意M的看法。博主的水平说实话是很不入流的。往细里讲,从c到gcc到arch,从tcp/ip到kernel到security
,无论是基础理论还是实际应用,完全是还没入门的水平。我早就说了,博主适合的位子是技术类编辑,就跟那个gigix是一路货。
Fisher_Yates算法 的意思是,从最后一张牌开始,和他前面的牌换,然后就不动了,
接下来每张牌都和它前面的牌换,不和后面的牌换。
这是为什么呢?有什么用意呢?
如果也可以和后面的牌换的话,会有什么缺陷呢?例如下面的这个程序:
void washcard1(int a[], int n)
{
int i=0;
int j=0;
int tmp;
for(;i<n;i++){
j = rand()%n;
tmp = a[i];
a[i]=a[j];
a[j]=tmp;
}
}
而且我觉得这个方法和“大多数人用的算法”差不多啊,为啥结果会不一样呢?
我自己试了,试了“大多数人的算法”和我上面的这个算法,都没发现问题,结果如下:
1000次:
112 102 89 96 105 101 96 84 107 108
101 107 103 85 114 106 105 91 96 92
113 90 81 99 93 118 98 93 103 112
78 85 122 105 110 92 86 106 105 111
98 101 98 103 86 101 115 98 101 99
97 110 100 111 89 112 94 110 82 95
94 103 98 107 88 112 103 98 98 99
98 112 81 96 107 89 109 105 104 99
93 96 121 95 104 89 98 100 111 93
116 94 107 103 104 80 96 115 93 92
10000次:
1008 1000 985 966 891 1048 1066 1005 983 1048
973 1053 957 979 988 1020 1039 966 1052 973
1047 982 997 988 939 1029 1020 972 1024 1002
1018 1038 1005 984 1066 1008 969 970 961 981
992 1004 1000 1042 985 1028 942 996 1001 1010
981 975 1030 989 1034 987 978 1072 984 970
979 935 1015 1016 1003 952 1024 1051 1025 1000
1008 951 1016 1041 1115 994 951 978 937 1009
978 1006 1046 984 1001 958 1014 988 1013 1012
1016 1056 949 1011 978 976 997 1002 1020 995
谁能帮忙解答一下,谢谢!
带彩色的程序是怎么弄出来的?而且还带缩进的
我试了qsort的那个方法,1000次误差在20%以内,10000次在10%以内。
都没有出现200次的那种情况。
为啥?是我的程序有问题吗?
#include
#include
#include
#define MAXLEN 10
int TestArr[MAXLEN][MAXLEN];
typedef void (*FunType)(int a[], int n);
void tranverse_arr(int a[], int n, FunType fp);
void initial_arr(int a[], int n);
void print_arr(int a[], int n);
void shufflecard1(int a[], int n);
void shufflecard2(int a[], int n);
void shufflecard3(int a[], int n);
/*
void shufflecard4(int a[], int n);
void shufflecard5(int a[], int n);
*/
int main(void)
{
int i,j;
FILE *fp;
int ori[MAXLEN] = {0};
fp = fopen(“D:/b.txt”, “w”);
srand((unsigned) time(NULL));
tranverse_arr(ori, MAXLEN, initial_arr);
//tranverse_arr(ori, MAXLEN, print_arr);
tranverse_arr(ori, MAXLEN, shufflecard3);
tranverse_arr(ori, MAXLEN, print_arr);
for(i=0; i<1000; i++){
for(j=0; j<MAXLEN; j++){
TestArr[ori[j]][j]++;
}
tranverse_arr(ori, MAXLEN, shufflecard3);
}
for(i=0; i<MAXLEN; i++){
for(j=0; j<MAXLEN; j++){
fprintf(fp, "%d\t", TestArr[i][j]);
}
fprintf(fp, "\r\n");
}
fclose(fp);
return 0;
}
void tranverse_arr(int a[], int n, FunType fp)
{
fp(a, n);
}
void initial_arr(int a[], int n)
{
int i=0;
for(;i<n;i++){
a[i]=i;
}
}
void print_arr(int a[], int n)
{
int i=0;
for(;i<n;i++){
printf("%d ", a[i]);
}
printf("\n");
}
void shufflecard1(int a[], int n)
{
int i=0;
int j=0;
int tmp;
for(;i<n;i++){
j = rand()%n;
tmp = a[i];
a[i]=a[j];
a[j]=tmp;
}
}
void shufflecard2(int a[], int n)
{
int i, j, temp, idx;
const int suff_time = n;
for (idx=0; idx<suff_time; idx++){
i = rand() % n;
j = rand() % n;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
int cmp(const void *a, const void *b)
{
return rand()%3-1;
}
void shufflecard3(int a[], int n)
{
qsort((void *)a, n, sizeof(int), cmp);
}
最近在看《测试之美》这本书,开发是有必要了解测试的,在敏捷开发里还有个叫——测试驱动呢。。。
rand()的后面用取余%是错误的用法,特别当%后面的数字稍大(比如10000)时。
正确的做法是尽量取rand()返回值尽量靠左边的数字,而不是用%取末几位。原因可以猜猜看 :)
算法不错
@fuckm 支持你的说法,其实楼主追求的才是真的伪随机。例如,在某个小样本范围内,必须可以有某个值是超越10%的。如果考虑极限,甚至可以出现洗完以后和没洗时候是一样的结果。
洗牌的问题在于是否够随机,随机算法一旦测试没问题,剩下的就是个牌型打撒问题(交换顺序),的确,没有真随机,都是伪随机,伪随机算法很多,JAVA的JDK用的是PRNG算法,还有Mersenne Twister:
http://en.wikipedia.org/wiki/Mersenne_twister
总结楼主的意思,是在讲如何测试随机性,跟棋牌无关。
我写的第一个需要创建随机序列的程序就是用的Fisher_Yates算法(当然我现在才知道它叫这个名字),我觉得这才是最直观的想法啊……
如果从内核中获取随机数,能不能真正的随机呢,比如使用计算机时敲击键盘的时间间隔,移动鼠标的距离与间隔?
@james
你说的就是 Linux 的 /dev/random 的实现了,Linux 会把所有驱动程序产生的垃圾数据扔进熵池里面。关键的问题是,里面的数据是不能指定范围的。
十年前我还上学的时候用了很少的代码做过shuffle:
1 生成相同长度的随机数
2 按随机数大小对数据进行关联排序
随机程度和随机函数保持一致,复杂度与排序函数一致。
@Daniel
原理就是抽签啊,@高中数学
用Fisher_Yates算法多来几次,应该会比模拟手洗多次的效果要好啊
Fisher_Yates就是从后向前,保证每张牌与前面的牌随机交换一次,思路非常顺,至于效果,另说
那个大部分人的算法,就有问题了,首先,每次都是任意两张牌交换,那么交换n次就莫名奇妙了,n好像可以但是实际不能保证每张牌都参与过交换。
测试程序还是错的… 洗牌有这么难么?
C的rand()用的是线性生成随机数……考虑%P之后如果P不是一个素数产生的碰撞了吗。。