洗牌算法Fisher-Yates以及C语言随机数的产生
前些天在蘑菇街的面试中碰到一道洗牌的算法题,拿出来和大家分享一下!
原题是:54张有序的牌,如何无序的发给3个人?
这个题是运用经典的洗牌算法完成。首先介绍一种经典的洗牌算法--Fisher-Yates.现在大家在网上看到,大多是Fisher-Yates算法的变形。将本来O(n2),简化到了O(n).代码如下:
#include<stdio.h> #include <stdlib.h> void func(char *, int); void main() { char a[7] = {'a','b','c','d','e','f'}; func(a,6); //a[7] = '\0'; puts(a); } void func(char *date, int length){ char t; //t为交换字符空间 int i, j; while(--length){ srand(time(0)); i = rand()%(length+1); t = date[i]; date[i] = date[length]; date[length] = t; } }
算法的思路就是:
a,选中未操作的最后1个位置,在1至该位置之间产生一个随机数(包含这两个值),
b,然后交换这两个值,造成的结果就是在未操作的所有牌中随机的取一张放到未操作的牌底(数组尾),然后length-1使未操作的范围减一,
c,重复上面步骤,直到未操作的值为1停止
代码写的很清晰,也很简单。网上还有很多种方法。但通过大量的测试,可以给出肯定,改进后的Fisher-Yates算法,即上述代码,已经是洗牌算法中很好的一个了。即简单,随机性又好。所以如果在面试中给出上述答案,就基本可以进行下一个问题了。如果面试官还觉得不够。可以从代码的完整性和鲁棒性(剑指offer中有提到具体做法),如参数正确性,遇到无效输入等方面改进。或者优化随机值。
关于优化随机值,这里我们还可以再提一点。
首先必须肯定的一点是:计算机中的随机数并不是真正的随机数,它是一种伪随机数,是通过某些公式计算出来的随机数,通过改变式子中参数的值从而达到随机的效果。虽然是伪随机数,但其实已经可以达到我们编程中的要求了。
C语言中生成随机数的函数有4个,两两搭配使用:
int rand(void); 返回 0 ------- RAND_MAX 之间的一个 int 类型整数,该函数为非线程安全函数。并且生成随机数的性能不是很好,已经不推荐使用。
void srand(unsigned int seed); 设置种子值,一般与“当前时间 + 进程ID”作为种子,如果没用调用该函数,则通过rand返回的默认种子值为1。
long int random(void); 返回 0 ------- RAND_MAX 之间的一个 long 类型整数,该函数会产生一个非常大的随机值,最大为 16*((2**31)-1)。
random 函数使用非线性反馈随机数发生器生成默认大小为31个长整数表所返回的连续伪随机数。
void srandom(unsigned int seed); 设置种子值,一般与“当前时间 + 进程ID”作为种子,如果没用调用该函数,则通过random返回的默认种子值为1。
上面的这四个函数都是C语言产生随机数时用到的函数,
如果你使用 srandom 种植种子, 则你应该使用 random 返回随机数, 如果你使用 srand 种植种子, 则你应该使用rand返回随机数。
不过srand和rand官方已经不推荐使用。原因是产生随机数的性能不是很好, 另外是随机数的随机性没有random好, 再者就是不是线程安全。
至于rand的线程不安全,如果你在linux下看过它的源代码的话,这句话就很容易理解了。
/* 这两个函数是C库中产生随机数的程序。你需要先 使用srand()函数赋随机数种子值。然后再使用 rand()函数来产生随机数。但是产生随机数的算法 较简单,srandom()和random()函数是对这两个函数 的改良,用法也很类似。 */ #define RANDOM_MAX 0x7FFFFFFF static long my_do_rand(unsigned long *value) { /* 这个算法保证所产生的值不会超过(2^31 - 1) 这里(2^31 - 1)就是 0x7FFFFFFF。而 0x7FFFFFFF 等于127773 * (7^5) + 2836,7^5 = 16807。 整个算法是通过:t = (7^5 * t) mod (2^31 - 1) 这个公式来计算随机值,并且把这次得到的值,作为 下次计算的随机种子值。 */ long quotient, remainder, t; quotient = *value / 127773L; remainder = *value % 127773L; t = 16807L * remainder - 2836L * quotient; if (t <= 0) t += 0x7FFFFFFFL; return ((*value = t) % ((unsigned long)RANDOM_MAX + 1)); } static unsigned long next = 1; int my_rand(void) { return my_do_rand(&next); } void my_srand(unsigned int seed) { next = seed; }
从代码中可以清晰的看到,srand()函数是将参数赋值给了一个全局变量,这也是为什么srand()使用int型参数且没有返回值却可以影响rand()的值。srand()修改了next的值后,rand()函数可能隐式的调用了该参数(间接调用)。看到这里,你就能明白为什么它是线程不安全函数,如果再多线程操作中,一个线程线调用srand,再未完成时另一个线程修改了next则该次操作无意义,这就是基本的多线程编程中的进程的同步与互斥。random()的改进就是基于这一点进行改进。除此之外,我们还可以看到rand()的返回值是基于一个类似于y=ax+b的函数计算出来的。而该表达式中a,b都是常量值,而这里的x就是随机种子,即通过srand()修改种子。
最后要提的一点是:
假如你想产生 1 ------10 之间的一个随机数, 建议您像下面这样编码
j = 1 + (int) (10.0 * (rand() / (RAND_MAX + 1.0)));
而不是下面这样的代码
j = 1 + (rand() % 10);
第一行代码的优势在于使用实型代替整形,数据是连续的,从而随机的更均匀,从而提高随机性。
如果你想要产生1-8的随机数,j = 1 + (int) (8.0 * (rand() / (RAND_MAX + 1.0))); //即可
1-x: j = 1 + (int) (x.0 * (rand() / (RAND_MAX + 1.0))); //即可