隨機數和洗牌算法

什麼是隨機數?通俗說法就是隨機產生的一個數,這個數預先不能計算出來的,而且全部可能出現的數字,機率應該是均勻的。所以隨機數應該知足至少如下兩點: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,則說明不是洗牌算法(存在誤判,多試幾回就能夠了)。

順便提一下網上的一些抽獎活動,尤爲是轉盤,是否是真正的隨機?答案留給看客!

相關文章
相關標籤/搜索