随机数和洗牌算法

什么是随机数?通俗说法就是随机产生的一个数,这个数预先不能计算出来的,而且全部可能出现的数字,几率应该是均匀的。所以随机数应该知足至少如下两点:java

  • 不可计算性,即不肯定性。
  • 机会均等,即每一个可能出现的数字必须几率相等。

如何产生随机数是一个具备挑战的问题,通常使用随机硬件产生,好比骰子、电子元件噪声、核裂变等。算法

在计算机编程中,咱们常常调用随机数产生器函数,但咱们必须清楚的一点是,通常直接调用软件的随机数产生器函数,产生的数字并非严格的随机数,而是经过必定的算法计算出来的(不知足随机数的不可计算性),咱们称它为伪随机数!编程

因为它具备相似随机的统计特征,在不是很严格的状况,使用软件方式产生伪随机相比硬件实现方式,成本更低而且操做简单、效率也更高!dom

那通常伪随机数如何产生呢? 通常是经过一个随机种子(好比当前系统时间值),经过某个算法(通常是位运算),不断迭代产生下一个数。好比c语言中的stdlib中rand_r函数(用的glibc):函数

/* This algorithm is mentioned in the ISO C standard, here extended
   for 32 bits.  */
int
rand_r (unsigned int *seed)
{
  unsigned int next = *seed;
  int result;
 
  next *= 1103515245;
  next += 12345;
  result = (unsigned int) (next / 65536) % 2048;
 
  next *= 1103515245;
  next += 12345;
  result <<= 10;
  result ^= (unsigned int) (next / 65536) % 1024;
 
  next *= 1103515245;
  next += 12345;
  result <<= 10;
  result ^= (unsigned int) (next / 65536) % 1024;
 
  *seed = next;
 
  return result;
}

而java中的Random类产生方法next()为:测试

protected int next(int bits) {
       long oldseed, nextseed;
       AtomicLong seed = this.seed;
       do {
           oldseed = seed.get();
           nextseed = (oldseed * multiplier + addend) & mask;
       } while (!seed.compareAndSet(oldseed, nextseed));
       return (int)(nextseed >>> (48 - bits));
   }

java中还有一个更精确的伪随机产生器java.security.SecurityRandom, 它继承自Random类,能够指定算法名称,next方法为:ui

final protected int next(int numBits) {
       int numBytes = (numBits+7)/8;
       byte b[] = new byte[numBytes];
       int next = 0;
 
       nextBytes(b);
       for (int i = 0; i < numBytes; i++) {
           next = (next << 8) + (b[i] & 0xFF);
       }
 
       return next >>> (numBytes*8 - numBits);
   }

固然这个类不只仅是重写了next方法,在种子设置等都进行了重写。this

最近有一道题:已知一个rand7函数,可以产生1~7的随机数,求一个函数,使其可以产生1~10的随机数。code

显然调用一次不可能知足,必须屡次调用!利用乘法原理,调用rand7() * rand7()能够产生1~49的随机数,咱们能够把结果模10(即取个位数)获得0~9的数,再加1,即产生1~10的数。但咱们还须要保证几率的机会均等性。显然1~49中,共有49个数,个位为0出现的次数要少1,不知足几率均等,若是直接这样计算,2~10出现的几率要比1出现的几率大!咱们能够丢掉一些数字,好比不要大于40的数字,出现大于40,就从新产生。排序

int rand10() {
    int ans;
    do {
        int i = rand7();
        int j = rand7();
        ans = i * j;
    } while(ans > 40);
    return ans % 10 + 1;
}

随机数的用途就不用多说了,好比取样,产生随机密码等。下面则着重说说其中一个应用--洗牌算法。

咱们可能接触比较多的一种状况是须要把一个无序的列表排序成一个有序列表。洗牌算法(shuffle)则是一个相反的过程,即把一个有序的列表(固然无序也无所谓)变成一个无序的列表。
这个新列表必须是随机的,即原来的某个数在新列表的位置具备随机性!

咱们假设有1~100共100个无重复数字。

很容易想到一种方案是:

  • 从第一张牌开始,利用随机函数生成器产生1~100的随机数,好比产生88,则看第88个位置有没有占用,若是没有占用则把当前牌放到第88位置,若是已经占用,则从新产生随机数,直到找到有空位置!

    首先必须认可这个方法是能够实现洗牌算法的。关键在于效率,首先空间复杂度是O(n),时间复杂度也是O(n),关键是越到后面越难找到空位置,大量时间浪费在求随机数和找空位置的。

第二中方案:

  • 从第一张牌开始,设当前位置牌为第i张,利用随机函数生成器产生1~100的随机数,好比产生88,则交换第i张牌和第88张牌。
    这样知足了空间是O(1)的原地操做,时间复杂度是O(n)。可是否可以保证每一个牌的位置具备机会均等性呢?

首先一个常识是:n张牌,利用随机数产生N种状况,则必须知足N可以整除n,这样就能给予每一个牌以N/n的机会(或者说权值),若是N不能整除n,必然机会不均等,即有些牌分配的机会多,有些少。
咱们知道100的全排列有100的阶乘种状况,而调用100次随机函数,共能够产生100^100种状况,而n^n 必然不能整除n!,具体证实不在这里叙述。
那咱们能够利用第二种方法改进,每次不是产生1~100的随机数,而是1~i的数字,则共有n!中状况,即N=n!,显然知足条件,且时间为O(n),空间为O(1).这也就是Fisher-Yates_shuffle算法,大多数库都使用的这种方法。
咱们看看java中Collections实现:

public static void shuffle(List<?> list, Random rnd) {
       int size = list.size();
       if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
           for (int i=size; i>1; i--)
               swap(list, i-1, rnd.nextInt(i));
       } else {
           Object arr[] = list.toArray();
 
           // Shuffle array
           for (int i=size; i>1; i--)
               swap(arr, i-1, rnd.nextInt(i));
 
           // Dump array back into list
           // instead of using a raw type here, it's possible to capture
           // the wildcard but it will require a call to a supplementary
           // private method
           ListIterator it = list.listIterator();
           for (int i=0; i<arr.length; i++) {
               it.next();
               it.set(arr[i]);
           }
       }
   }

除了首先判断可否随机访问,剩下的就是以上算法的实现了。

STL中实现:

// random_shuffle
 
template <class _RandomAccessIter>
inline void random_shuffle(_RandomAccessIter __first,
                           _RandomAccessIter __last) {
  __STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);
  if (__first == __last) return;
  for (_RandomAccessIter __i = __first + 1; __i != __last; ++__i)
    iter_swap(__i, __first + __random_number((__i - __first) + 1));
}
 
template <class _RandomAccessIter, class _RandomNumberGenerator>
void random_shuffle(_RandomAccessIter __first, _RandomAccessIter __last,
                    _RandomNumberGenerator& __rand) {
  __STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);
  if (__first == __last) return;
  for (_RandomAccessIter __i = __first + 1; __i != __last; ++__i)
    iter_swap(__i, __first + __rand((__i - __first) + 1));
}

如何测试洗牌算法具备随机性呢?其实很简单,调用洗牌算法N次,牌数为n,统计每一个数字出如今某个位置的出现次数,构成一个矩阵n * n,若是这个矩阵的值都在N/n左右,则洗牌算法好。好比有100个数字,统计一万次,则每一个数字在某个位置的出现次数应该在100左右。

洗牌算法的应用也很广,好比三国杀游戏、斗地主游戏等等。讲一个最多见的场景,就是播放器的随机播放。有些播放器的随机播放,是每次产生一个随机数来选择播放的歌曲,这样就有可能尚未听完全部的歌前,又听到已经听过的歌。另外一种就是利用洗牌算法,把待播放的歌曲列表shuffle。如何判断使用的是哪种方案呢? 很简单,若是点上一首还能回去,则利用的是洗牌算法,若是点上一首又是另一首歌,则说明使用的是随机产生方法。好比上一首是3,如今是18,点上一首,若是是3说明采用的洗牌算法,若是不是3,则说明不是洗牌算法(存在误判,多试几回就能够了)。

顺便提一下网上的一些抽奖活动,尤为是转盘,是否是真正的随机?答案留给看客!

相关文章
相关标签/搜索