嗯,自从前几天,我写了一篇博文《我以前用过的一个洗牌算法》(
http://tonyxiaohome.blog.51cto.com/925273/302220
)之后,有很多网友和我讨论,并提出了自己的算法。
这里我声明一件事啊,我非常欢迎大家就纯技术问题和我讨论,恶意PK,我自然没有好脸色,不过,技术讨论,还是欢迎的。
尺有所短,寸有所长,我从来没有说过我的东西对完了,也没说过我的东西都是圣旨,一点不能改的,无论是过去、现在还是以后,我都欢迎大家就技术问题和我展开讨论,批评也可以啊,只要说得有理,我就改。
我几乎每篇博文不是都说过嘛,“一家之言,欢迎拍砖”哈。
嗯,这位朋友呢,说实话哈,算法可以,不过,这个代码呢,我看着有点晕:
- #include <stdio.h>
- #include <stdlib.h>
- #include <time.h>
- int d[6];
- int i,n,a,b,t;
- int c,j;
- void Wash()
- {
- srand((unsigned int)time(NULL));
- printf("shuffle 0..n-1 demo\n");
- for (n=1;n<=5;n++)
- {/* 测试1~5个元素 */
- printf("_____n=%d_____\n",n);
- j=1;
- for (c=1;c<=n;c++)
- j=j*c;/* j为n! */
- j*=n*2;
- for (c=1;c<=j;c++)
- {/* 测试n*2*n!次 */
- for (i=0;i<n;i++)
- d[i]=i;/* 填写0~n-1 */
- for (i=n;i>0;i--)
- {/* 打乱0~n-1 */
- a=i-1;
- b=rand()%i;
- if (a!=b)
- {
- t=d[a];
- d[a]=d[b];
- d[b]=t;
- }
- }
- printf("%04d:",c);
- for (i=0;i<n;i++)
- printf("%d",d[i]);
- printf("\n");
- }
- }
- printf("shuffle 1..n demo\n");
- for (n=1;n<=5;n++)
- {/* 测试1~5个元素 */
- printf("_____n=%d_____\n",n);
- j=1;
- for (c=1;c<=n;c++)
- j=j*c;/* j为n! */
- j*=n*2;
- for (c=1;c<=j;c++)
- {/* 测试n*2*n!次 */
- for (i=1;i<=n;i++)
- d[i]=i;/* 填写1~n */
- for (i=n;i>1;i--)
- {/* 打乱1~n */
- a=i;
- b=rand()%i+1;
- if (a!=b)
- {
- t=d[a];
- d[a]=d[b];
- d[b]=t;
- }
- }
- printf("%04d:",c);
- for (i=1;i<=n;i++) printf("%d",d[i]);
- printf("\n");
- }
- }
- }
说实话,这还是我重新排版过的,原来的版本还要晕一点。呵呵。
我当时看了15分钟,最终确认,我晕了。只有给他写了一封回信:
看了,能work,能看出来,在1~5之间乱序,程序本身是成功的。
不过,说实话我没看懂原理,主要是程序太难读了。
根据我写程序的经验,我给点建议吧,仅供参考:
1、我团队的规矩,测试代码和正式代码的质量要求相同,不能因为测试代码就放宽要求,均要求严格按照《0bug-C/C++商用工程之道》第三章的无错化程序设计方法执行。
2、不建议使用全局变量,所有变量声明时建议同时赋初值,避免“野变量”,因此,有个推论,每一行仅仅处理一个变量的声明,这样看起来非常清晰。
3、数组定义为6,比5多,d[6],我一直没看懂这多出来的一个单元格是做什么的,如果仅仅是为了防止溢出错误,则应该是修订自己的程序,避免溢出,而不能采用这个打补丁的办法。
4、我的C/C++无错化程序设计方法,规定for只有一种写法,是有道理的,for(i=0;i<n;i++),这里面暗示了所有n次的循环,i均是从0到n-1,恰好符合d[5]这类数组单元的0~4的有效index 的访问需求,建议以后采用这类统一格式处理for。像目前的这个for (n=1;n<=5;n++),说实话,我看着就头大,因为我脑子里面一直想,那个d[0]在做什么?
5、我的C/C++无错化程序设计方法,强调简单化代码,每个函数内部只写一重循环,目的就是简化代码,让人好读懂,函数不怕多,怕的是函数太长,建议以后可以把这个函数分拆来处理。
6、我的C/C++无错化程序设计方法,强调函数名和变量名严格定名,就是为了把每句话都写得像个英文短句一样,大家看起来一目了然,避免读者误读。目前的写法,我发呆了15分钟,说实话,最后晕了。
7、这算是复杂逻辑,建议在前面专门注释一段,说明你采用的原理,并且举例子说明,这样我看起来,起码有个脉络,现在的写法,我理解起来很费解。
8、两段不同代码的差异在哪里我没能看出来,能不能说明一下?
9、程序中间的注释,建议采用C++模式,即“//”,这样,我调试,需要暂时隐掉大段代码时,可以使用“/*...*/”,这是我的一个习惯,也是团队的习惯,大家觉得很好用,像现在这样“/*...*/”已经被大量采用的时候,我作为看代码的人,再想大段隐掉就很困难。
10、“for (c=1;c<=n;c++) j=j*c;/* j为n! */ j*=n*2;”这段一直没看懂,为什么阶乘后,还要再乘个n的两倍,有什么数学道理?
11、“t=d[a];d[a]=d[b];d[b]=t;”,这明显是交换算法,并且在程序中出现两次,我团队的规定,每一种逻辑,只允许出现一次,一次写对,以后只调用,不重写,避免无谓的笔误bug,建议单独提出来,做个函数,哪怕是inline,也好过现在的写法。
12、“for (i=n;i>0;i--){/* 打乱0~n-1 */a=i-1;b=rand()%i;”这段看起来非常晕,因为我知道前面你赋值的时候,只给了1~5这5个单元赋值,而这个时候,显然a的取值范围是n-1~0,就是4~0,我感觉这里算法上就是有bug,不对。
先这么多吧,我呢,水平有限,可能暂时还没有理解到这个算法的深意,因此,暂时也提不出太好的算法建议。
欢迎讨论。
嗯,你这段代码我觉得很有代表意义,如果你不反对的话,我想发篇博文,列出这个代码,给大家学习一下。
你意下如何?
不过,这位朋友显然是一位非常认真的朋友,他随即给我了回信:
不过,说实话我没看懂原理,主要是程序太难读了。
根据我写程序的经验,我给点建议吧,仅供参考:
1、我团队的规矩,测试代码和正式代码的质量要求相同,不能因为测试代码就放宽要求,均要求严格按照《0bug-C/C++商用工程之道》第三章的无错化程序设计方法执行。
2、不建议使用全局变量,所有变量声明时建议同时赋初值,避免“野变量”,因此,有个推论,每一行仅仅处理一个变量的声明,这样看起来非常清晰。
3、数组定义为6,比5多,d[6],我一直没看懂这多出来的一个单元格是做什么的,如果仅仅是为了防止溢出错误,则应该是修订自己的程序,避免溢出,而不能采用这个打补丁的办法。
4、我的C/C++无错化程序设计方法,规定for只有一种写法,是有道理的,for(i=0;i<n;i++),这里面暗示了所有n次的循环,i均是从0到n-1,恰好符合d[5]这类数组单元的0~4的有效index 的访问需求,建议以后采用这类统一格式处理for。像目前的这个for (n=1;n<=5;n++),说实话,我看着就头大,因为我脑子里面一直想,那个d[0]在做什么?
5、我的C/C++无错化程序设计方法,强调简单化代码,每个函数内部只写一重循环,目的就是简化代码,让人好读懂,函数不怕多,怕的是函数太长,建议以后可以把这个函数分拆来处理。
6、我的C/C++无错化程序设计方法,强调函数名和变量名严格定名,就是为了把每句话都写得像个英文短句一样,大家看起来一目了然,避免读者误读。目前的写法,我发呆了15分钟,说实话,最后晕了。
7、这算是复杂逻辑,建议在前面专门注释一段,说明你采用的原理,并且举例子说明,这样我看起来,起码有个脉络,现在的写法,我理解起来很费解。
8、两段不同代码的差异在哪里我没能看出来,能不能说明一下?
9、程序中间的注释,建议采用C++模式,即“//”,这样,我调试,需要暂时隐掉大段代码时,可以使用“/*...*/”,这是我的一个习惯,也是团队的习惯,大家觉得很好用,像现在这样“/*...*/”已经被大量采用的时候,我作为看代码的人,再想大段隐掉就很困难。
10、“for (c=1;c<=n;c++) j=j*c;/* j为n! */ j*=n*2;”这段一直没看懂,为什么阶乘后,还要再乘个n的两倍,有什么数学道理?
11、“t=d[a];d[a]=d[b];d[b]=t;”,这明显是交换算法,并且在程序中出现两次,我团队的规定,每一种逻辑,只允许出现一次,一次写对,以后只调用,不重写,避免无谓的笔误bug,建议单独提出来,做个函数,哪怕是inline,也好过现在的写法。
12、“for (i=n;i>0;i--){/* 打乱0~n-1 */a=i-1;b=rand()%i;”这段看起来非常晕,因为我知道前面你赋值的时候,只给了1~5这5个单元赋值,而这个时候,显然a的取值范围是n-1~0,就是4~0,我感觉这里算法上就是有bug,不对。
先这么多吧,我呢,水平有限,可能暂时还没有理解到这个算法的深意,因此,暂时也提不出太好的算法建议。
欢迎讨论。
嗯,你这段代码我觉得很有代表意义,如果你不反对的话,我想发篇博文,列出这个代码,给大家学习一下。
你意下如何?
不过,这位朋友显然是一位非常认真的朋友,他随即给我了回信:
原理打个比方:
若干张扑克牌,最上面放一张纸条。
①把纸条挪到它下面那张牌的下面,即往下一张牌。如果此时纸条已经到达所有牌的下面,结束。
②在紧帖纸条上面那一张和纸条下面所有牌中随机选一张和紧帖纸条上面那一张交换位置。(当然如果本来选的就是紧帖纸条上面那一张,就不用交换了)
重复上面步骤①和②直到结束。
若干张扑克牌,最上面放一张纸条。
①把纸条挪到它下面那张牌的下面,即往下一张牌。如果此时纸条已经到达所有牌的下面,结束。
②在紧帖纸条上面那一张和纸条下面所有牌中随机选一张和紧帖纸条上面那一张交换位置。(当然如果本来选的就是紧帖纸条上面那一张,就不用交换了)
重复上面步骤①和②直到结束。
数组多定义一个的原因是因为下面要演示对0~n-1和1~n两种下标范围的代码。因为参考这个代码的人可能两种情况只能选用其一或两种都要在不同场合使用。
两端代码算法上没有差异,只是表明当下标范围不同时写法不同。
注释我原来都是采用//风格的,在Win-TC里面编译时提示不支持此风格所以让其自动替换为/**/风格了。
在回帖时被连成长句也正好说明在这种特殊情况下/**/风格比//风格好。当然我还是支持//风格的。
在回帖时被连成长句也正好说明在这种特殊情况下/**/风格比//风格好。当然我还是支持//风格的。
测试次数选用n*2*n!只是我开始想用n*n!得到所有排列,结果在n很小时失败,所以随手改成n*2*n!基本达到目的而已,没什么数学道理。
正如你的很多建议,我这段代码如果直接拿去作为一个工程代码的一部分,是远远不够的。
但用来演示对0~n-1和1~n两种下标范围的数组进行洗牌,应该除了没加上原理说明的注释外,也够用了。
但用来演示对0~n-1和1~n两种下标范围的数组进行洗牌,应该除了没加上原理说明的注释外,也够用了。
很高兴和你交流、讨论。
也很乐意看到在你的新博文中引用我这段代码。
这时候,我才真正看明白,嗯,我的回复如下:
这样我就明白了。
你是以一根指针,顺序遍历数组每个元素,确保每个元素至少有一次交换机会,这样牌会洗得更烂。
我的方法是随机选择两个,由于随机数的不确定性,因此,我并不保证每个元素都确定拥有至少一次被洗牌的机会。看起来,从单纯洗烂的角度上讲呢,我的方法没你洗得烂。
嗯,我当初设定这个双随机交换时,有个考量,是因为我们平时洗盘是,牌分两半,大拇指随机蹦出,其实是把牌随机对插,这时候,有时候我们大拇指松的快点呢,其实可能会有几张牌被连着弹出去,就是说其顺序没有被打乱,这在洗一些很脏的旧牌的时候,由于牌中间有粘连,很容易出现。
我当时想双随机,没有以一个顺序遍历随机,就是想真实模拟这种情况,我想的是:“某一张或几张牌不被洗到,其实也是生活中随机洗牌的合理情况”,所以,我没有刻意去遍历,保证每张牌都有被洗到的机会,其根源来自于此。
嗯,我这么说,你能理解我的意思吗?不被洗,保留原位置信息,其实也是随机性的一种体现。既然是随机,就是说,选不到,洗不到,也是随机的一种。
因此,我选择了我前文的洗牌法。
嗯,欢迎探讨哈。
你是以一根指针,顺序遍历数组每个元素,确保每个元素至少有一次交换机会,这样牌会洗得更烂。
我的方法是随机选择两个,由于随机数的不确定性,因此,我并不保证每个元素都确定拥有至少一次被洗牌的机会。看起来,从单纯洗烂的角度上讲呢,我的方法没你洗得烂。
嗯,我当初设定这个双随机交换时,有个考量,是因为我们平时洗盘是,牌分两半,大拇指随机蹦出,其实是把牌随机对插,这时候,有时候我们大拇指松的快点呢,其实可能会有几张牌被连着弹出去,就是说其顺序没有被打乱,这在洗一些很脏的旧牌的时候,由于牌中间有粘连,很容易出现。
我当时想双随机,没有以一个顺序遍历随机,就是想真实模拟这种情况,我想的是:“某一张或几张牌不被洗到,其实也是生活中随机洗牌的合理情况”,所以,我没有刻意去遍历,保证每张牌都有被洗到的机会,其根源来自于此。
嗯,我这么说,你能理解我的意思吗?不被洗,保留原位置信息,其实也是随机性的一种体现。既然是随机,就是说,选不到,洗不到,也是随机的一种。
因此,我选择了我前文的洗牌法。
嗯,欢迎探讨哈。
这是这次关于洗牌算法到目前为止我们的讨论结果,我想了一下,觉得有必要把这些信息的详细情况,写篇博文来给大家看,帮助大家理解一下随机数的认识。
这里我说一点自己的看法:
随机数求解时,最关键的是不能预设立场,即我们既不能断言,某个数一定存在,也不能断言,某个数一定不存在,这些都不是真正的随机,是被其他参量(程序员的思想)影响过的伪随机数。
好比我们甩骰子,第一把我们甩了个6点,第二把不一定是6点,这大家都知道,否则就不是随机数,但是,我发现很少有程序员朋友关注一个细节,我们同时也不能预先把6点排除,即影响第二把的结果一定“不是”6点,这也不是随机。
我的随机洗牌算法,使用双随机位置交换,主要就是不想预设立场,我既不想某张牌一定不被交换,但是,我同时也不想某张牌一定被交换,我认为这都不是真正的随机,所以我选择了现在的算法。
这位朋友的算法,有一定成分的预设立场在里面,我认为其随机度不够。
嗯,这算我一家之言吧,欢迎大家继续讨论哈。
=======================================================
在线底价购买《0bug-C/C++商用工程之道》
(直接点击下面链接或拷贝到浏览器地址栏)
http://s.click.taobao.com/t_3?&p=mm_13866629_0_0&n=23&l=http%3A%2F%2Fsearch8.taobao.com%2Fbrowse%2F0%2Fn-g%2Corvv64tborsvwmjvgawdkmbqgboq---g%2Cgaqge5lhebbs6qzlfmqmttgtyo42jm6m22xllqa-------------1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20---40--coefp-0-all-0.htm%3Fpid%3Dmm_13866629_0_0
在线底价购买《0bug-C/C++商用工程之道》
(直接点击下面链接或拷贝到浏览器地址栏)
http://s.click.taobao.com/t_3?&p=mm_13866629_0_0&n=23&l=http%3A%2F%2Fsearch8.taobao.com%2Fbrowse%2F0%2Fn-g%2Corvv64tborsvwmjvgawdkmbqgboq---g%2Cgaqge5lhebbs6qzlfmqmttgtyo42jm6m22xllqa-------------1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20---40--coefp-0-all-0.htm%3Fpid%3Dmm_13866629_0_0
肖舸