隨機數的產生——————摘錄

說明:寫做本文的出發點是最近和一個有3年開發經驗的.NET開發人員聊天,他跟我說常常沒有思路,在實際開發中我也見過一個具備45年開發經驗的開發人員幾乎沒有靈活變通的能力,因此打算寫一系列文章,在這個系列文章中我會主要講解解題的思路,而不是講述什麼新技術新特性,借這個系列文章爲初中級開發者瞭解遇到問題別人是如何思考和解決的。固然,若是你的思路比本文提到的更好,歡迎指出來,同時若是你對本系列文章有更好的建議或者有平常中的一些典型問題,請給我聯繫,咱們共同探討。目前我暫時能想到的有不重複隨機數產生問題、字符串與數值轉換的問題、特殊的數據庫鎖問題、訪客來路追蹤問題、在線用戶統計問題、統計用戶訪問頁面偏好問題。
好了,我如今開始本篇的講述。本篇的最原始形態是來源於我早年作的一個Java SE應用軟件,它是用來模擬彩票投注站的選好軟件的。應爲在早年Java SE中用swing作界面佈局是一件比較痛苦的事情,因此後來我從新用C#作了一個。這個問題的原型就是解決雙色球隨機選號的問題,咱們知道雙色球紅色球共包含13333個紅色號碼球及11616個藍色號碼球,一注雙色球號碼應包括6個紅色球號碼和1個藍色球號碼。藍色號碼球很好解決,隨機從11616個數字中隨機選取一個就好了。可是紅色球就存在這樣同樣問題,每次選取的紅色球不能與本注中已經選取的號碼重複,這個問題歸結爲生成不重複隨機數問題。
在本篇我就怎麼生成不重複的紅色球展開討論。
解題思路一
在早期的Java中不包含泛型,只能使用ArrayList,因此我是用ArrayList來實現的。在Java中的ArrayListC#中的ArrayList在用法上是很類似的(這就是爲何高手常常說掌握一門語言以後再去掌握另外一門語言是很容易的事情,應爲思想是相通的,呵呵)。在這裏我最想一想到的就是使用循環,每次循環中隨機生成一個隨機數,判斷一下這個隨機數是否已經在本注中使用,若是沒有使用就將這個號碼保存到結果中去,反之則進行下一輪循環,循環的結束條件就是生成了知足要求的6個數字。接觸到泛型以後,我知道在這裏我所使用的數據類型是int類型(固然也可使用byte類型),若是使用ArrayList保存int這樣的值類型數據會存在着裝箱和拆箱操做,帶來沒必要要的性能損失,因此針對這種集合中數據類型單一的狀況能夠考慮泛型集合,因而獲得了下面的代碼:

 

  1. /// <summary>     
  2. /// 從1到33中任意選取不重複的6個隨機數     
  3. /// </summary>     
  4. /// <returns></returns>     
  5. public List<int> GenerateNumber1()     
  6. {     
  7.     //用於保存返回的結果     
  8.     List<int> result = new List<int>(6);     
  9.     Random random = new Random();     
  10.     int temp = 0;     
  11.     //若是返回的結果集合中實際的元素個數小於6個     
  12.     while (result.Count < 6)     
  13.     {     
  14.         //在[1,34)區間任意取一個隨機整數     
  15.         temp = random.Next(1, 34);     
  16.         if (!result.Contains(temp))     
  17.         {     
  18.             //若是在結果集合中不存在這個數,則添加這個數     
  19.             result.Add(temp);     
  20.         }     
  21.     }     
  22.     //result.Sort();//對返回結果進行排序     
  23.     return result;     

 

固然,上面這種思路是能夠實現的,可是每次隨機生成一個隨機數都要判斷在結果集合中是否已經存在這個數,若是存在還要繼續下一個循環,這樣一來並非每一輪循環都能生成一個有效(即不重複)的隨機數,而且result.Contains(temp)儘管看起來只有一句,但實際在內部仍是要經過循環來判斷,效率仍是較低。假若有一天有我的看到這篇文章,他想:很好,我終於能夠試試了,我要從110000個數中取出9999個不重複的隨機數,用上面的這個方法,可能很長時間都得不到結果(不要覺得沒有這樣的人,我就碰見多屢次不會變通的人,實際上最好的解決辦法就是從10000中隨機去掉一個就能夠了,而不是照搬上面的套路)。
解題思路二
剛纔說到在方法一中並非每一輪循環都能生成一個有效的、不重複的隨機數,那麼有沒有這樣的辦法,保證每一輪循環都能生成一個有效地、不重複的隨機數呢?答案是有的。
具體作法是這樣的,咱們將初始化一個容器集合,在這個容器集合中包含了全部可能的值,而後每次隨機從這個容器集合中隨機選取一個值保存到結果集合中去,以後咱們就從容器集合中將這個已經使用過的值刪除掉,而後再進行下一輪的循環。既然都已經從容器集合中刪除掉了,天然在下一輪循環中隨機從容器集合中取一個值,這個值天然不會重複了。由於這個集合的容量是可變的,那麼天然也是使用泛型集合了,代碼以下:

 

  1. /// <summary>     
  2. /// 從1到33中任意選取不重複的6個隨機數     
  3. /// </summary>     
  4. /// <returns></returns>     
  5. public List<int> GenerateNumber2()     
  6. {     
  7.     //用於存放1到33這33個數     
  8.     List<int> container = new List<int>(33);     
  9.     //用於保存返回結果     
  10.     List<int> result = new List<int>(6);     
  11.     Random random = new Random();     
  12.     for (int i = 1; i <= 33; i++)     
  13.     {     
  14.         container.Add(i);     
  15.     }     
  16.     int index = 0;     
  17.     int value = 0;     
  18.     for (int i = 1; i <= 6; i++)     
  19.     {     
  20.         //從[0,container.Count)中取一個隨機值,保證這個值不會超過container的元素個數     
  21.         index = random.Next(0, container.Count);     
  22.         //以隨機生成的值做爲索引取container中的值     
  23.         value = container[index];     
  24.         //將隨機取得值的放到結果集合中     
  25.         result.Add(value);     
  26.         //從容器集合中刪除這個值,這樣會致使container.Count發生變化     
  27.         container.RemoveAt(index);     
  28.         //注意這一句與上面一句能達到一樣效果,可是沒有上面一句快     
  29.         //container.Remove(value);     
  30.     }     
  31.     //result.Sort();排序     
  32.     return result;     

 

通過這麼一改動,確實能作到每次循環都能生成一個惟一的有效的不重複的數,這樣一來就能作到在M個數中選取N個數時只須要循環M×N次就能夠了(MN,而且都是正整數)。不過也有人會說,我如今還在維護一個.NET1.1的項目,我這裏也有相似的需求,但是在.NET2.0如下版本中是沒有泛型的,你能給我想個辦法嗎?個人答案是能夠的。
解題方法三
在這個方法中咱們不適用泛型集合,只使用數組,這樣一來這種作法就能夠適用於CJavaPHP等語言和.NET1.1中了。
思路以下,首先使用一個數組做爲存儲全部可能值的容器集合,而後經過循環每次生成一個隨機值,這個值未來會做爲下標來訪問容器集合中的數值。由於數組是不可變集合,咱們不能將已經使用數值從數組中刪除,而且它們是簡單的數據類型咱們不可能給每一個數值增長一個屬性表示數值是否已經被使用過了,那該怎麼辦呢?辦法就是每次從可用的下標集合中隨機生成一個值,而後以這個值做爲索引從容器集合中獲得相應的值保存到結果集合中,除此以外再將這個已經使用過的值與數組中最後一個沒有使用到的值互換位置,而後下一輪再在全部沒有使用過的值中從新再取一個值。代碼以下:

 

  1. public int[] GenerateNumber3()     
  2. {     
  3.     //用於存放1到33這33個數     
  4.     int[] container = new int[33];     
  5.     //用於保存返回結果     
  6.     int[] result = new int[6];     
  7.     Random random = new Random();     
  8.     for (int i = 1; i <= 33; i++)     
  9.     {     
  10.         container[i - 1] = i;     
  11.     }     
  12.     int index = 0;     
  13.     int value = 0;     
  14.     for (int i = 0; i < 6; i++)     
  15.     {     
  16.         //從[1,container.Count + 1)中取一個隨機值,保證這個值不會超過container的元素個數     
  17.         index = random.Next(1, container.Length-1-i);     
  18.         //以隨機生成的值做爲索引取container中的值     
  19.         value = container[index];     
  20.         //將隨機取得值的放到結果集合中     
  21.         result[i]=value;     
  22.         //將剛剛使用到的從容器集合中移到末尾去     
  23.         container[index] = container[container.Length - i-1];     
  24.         //將隊列對應的值移到隊列中     
  25.         container[container.Length - i-1] = value;     
  26.     }     
  27.     //result.Sort();排序     
  28.     return result;     

 

這樣一來,問題獲得瞭解決了。這種作法也能夠移植到不支持泛型的版本或者語言當中。
 
再來一點變更
上面咱們處理的都是連續的狀況,假如萬一讓咱們在不連續的集合中隨機選擇5個不重複的,好比在某個班50個學生中隨機抽取5個學生來,貌似上面的作法不行了?其實否則,依然能夠沿用這種思路,好比使用上面的第三種辦法即GenerateNumber3(),無非就是聲明一個字符串數組,在這個字符串數組中存放全班全部同窗的姓名,而後按照下標來隨機取5個姓名便可(具體代碼這裏省略)。
不過對這種狀況還有不一樣的,好比在快女和春晚中都有短信投票的環節,最後會從全部發送短信的手機號中隨機抽取幾個手機號碼做爲中獎號碼(爲了簡化,將一號多投的狀況視做一次,而且假設沒有廉租房「連號」的「公平」狀況),可能有人會想到照搬上面的狀況。實際上在這種狀況下不適合,有可能隨機數的集合至關大,這樣就不適合在內存中存儲了,能夠考慮使用數據庫存儲而後利用數據庫的隨機函數,在不一樣的數據庫中取隨機記錄的函數可能會不一樣。好比在MySQL中以下(假設表名爲table_name)
select * from `table_name` order by rand() limit 0,10
而在SQL Server中會是以下(假設表名爲table_name)
select top 10 * from [table_name] order by newid()
至於在Oracle中如何隨機抽取記錄,你們google一下吧。
固然,這個狀況還能夠再複雜一下,可能電視臺針對短信參與的用戶要分一二三等獎,一等獎1名,二等獎2名,三等獎3名,記念獎50名,那麼狀況就會稍微再複雜一點,不過也就是再增長一個字段而已,這個字段表示當前的號碼是否已經中過獎,每次選取號碼時只選擇那些不曾中獎過的號碼便可。
總結
關於不重複隨機數生成的問題我在七八年前就遇到過,四五年前的時候曾經作過總結,最近看到有人在討論這個問題,因而就又從新撿起這個話題了。如今撿起這個話題的目的不是想再簡單介紹可能的幾種算法,而是從思路上去說明,而且將狀況慢慢複雜化,想要說明的是程序員們(不限於.NET程序員)不要用固定的思路去解決問題,可能一樣的要求在不一樣的場合下會有不一樣的作法。明白了思路才能真正作到以不變應萬變,學會一兩個控件的用法或者多指導一兩個API並不算什麼本領,可以在遇到之前沒有碰到過的問題時迅速簡化解決思路纔是本領,另外一種本領就是遇到錯誤時如何快速根據經驗定位錯誤產生緣由的本領。
 
周公
相關文章
相關標籤/搜索