轉(zhuǎn)帖|其它|編輯:郝浩|2010-08-26 15:04:57.000|閱讀 1825 次
概述:寫作本文的出發(fā)點是最近和一個有3年開發(fā)經(jīng)驗的.NET開發(fā)人員聊天,他跟我說經(jīng)常沒有思路,在實際開發(fā)中我也見過一個具有4、5年開發(fā)經(jīng)驗的開發(fā)人員幾乎沒有靈活變通的能力,所以打算寫一系列文章,在這個系列文章中我會主要講解解題的思路,而不是講述什么新技術(shù)新特性,借這個系列文章為初中級開發(fā)者了解遇到問題別人是如何思考和解決的。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
本篇的最原始形態(tài)是來源于我早年做的一個Java SE應(yīng)用軟件,它是用來模擬彩票投注站的選好軟件的。應(yīng)為在早年Java SE中用swing做界面布局是一件比較痛苦的事情,所以后來我重新用C#做了一個。這個問題的原型就是解決雙色球隨機選號的問題,我們知道雙色球紅色球共包含1到33這33個紅色號碼球及1到16這16個藍色號碼球,一注雙色球號碼應(yīng)包括6個紅色球號碼和1個藍色球號碼。藍色號碼球很好解決,隨機從1到16這16個數(shù)字中隨機選取一個就行了。但是紅色球就存在這樣一樣問題,每次選取的紅色球不能與本注中已經(jīng)選取的號碼重復,這個問題歸結(jié)為生成不重復隨機數(shù)問題。
在本篇我就怎么生成不重復的紅色球展開討論。
解題思路一
在早期的Java中不包含泛型,只能使用ArrayList,所以我是用ArrayList來實現(xiàn)的。在Java中的ArrayList和C#中的ArrayList在用法上是很相似的(這就是為什么高手經(jīng)常說掌握一門語言之后再去掌握另一門語言是很容易的事情,應(yīng)為思想是相通的,呵呵)。在這里我最想想到的就是使用循環(huán),每次循環(huán)中隨機生成一個隨機數(shù),判斷一下這個隨機數(shù)是否已經(jīng)在本注中使用,如果沒有使用就將這個號碼保存到結(jié)果中去,反之則進行下一輪循環(huán),循環(huán)的結(jié)束條件就是生成了滿足要求的6個數(shù)字。接觸到泛型之后,我知道在這里我所使用的數(shù)據(jù)類型是int類型(當然也可以使用byte類型),如果使用ArrayList保存int這樣的值類型數(shù)據(jù)會存在著裝箱和拆箱操作,帶來不必要的性能損失,所以針對這種集合中數(shù)據(jù)類型單一的情況可以考慮泛型集合,于是得到了下面的代碼:
/// <summary>
/// 從1到33中任意選取不重復的6個隨機數(shù)
/// </summary>
/// <returns></returns>
public List<int> GenerateNumber1()
{
//用于保存返回的結(jié)果
List<int> result = new List<int>(6);
Random random = new Random();
int temp = 0;
//如果返回的結(jié)果集合中實際的元素個數(shù)小于6個
while (result.Count < 6)
{
//在[1,34)區(qū)間任意取一個隨機整數(shù)
temp = random.Next(1, 34);
if (!result.Contains(temp))
{
//如果在結(jié)果集合中不存在這個數(shù),則添加這個數(shù)
result.Add(temp);
}
}
//result.Sort();//對返回結(jié)果進行排序
return result;
當然,上面這種思路是可以實現(xiàn)的,但是每次隨機生成一個隨機數(shù)都要判斷在結(jié)果集合中是否已經(jīng)存在這個數(shù),如果存在還要繼續(xù)下一個循環(huán),這樣一來并不是每一輪循環(huán)都能生成一個有效(即不重復)的隨機數(shù),并且result.Contains(temp)盡管看起來只有一句,但實際在內(nèi)部還是要通過循環(huán)來判斷,效率還是較低。假如有一天有個人看到這篇文章,他想:很好,我終于可以試試了,我要從1到10000個數(shù)中取出9999個不重復的隨機數(shù),用上面的這個方法,可能很長時間都得不到結(jié)果(不要以為沒有這樣的人,我就遇見多多次不會變通的人,實際上最好的解決辦法就是從10000中隨機去掉一個就可以了,而不是照搬上面的套路)。
解題思路二
剛才說到在方法一中并不是每一輪循環(huán)都能生成一個有效的、不重復的隨機數(shù),那么有沒有這樣的辦法,保證每一輪循環(huán)都能生成一個有效地、不重復的隨機數(shù)呢?答案是有的。
具體做法是這樣的,我們將初始化一個容器集合,在這個容器集合中包含了所有可能的值,然后每次隨機從這個容器集合中隨機選取一個值保存到結(jié)果集合中去,之后我們就從容器集合中將這個已經(jīng)使用過的值刪除掉,然后再進行下一輪的循環(huán)。既然都已經(jīng)從容器集合中刪除掉了,自然在下一輪循環(huán)中隨機從容器集合中取一個值,這個值自然不會重復了。因為這個集合的容量是可變的,那么自然也是使用泛型集合了,代碼如下:
/// <summary>
/// 從1到33中任意選取不重復的6個隨機數(shù)
/// </summary>
/// <returns></returns>
public List<int> GenerateNumber2()
{
//用于存放1到33這33個數(shù)
List<int> container = new List<int>(33);
//用于保存返回結(jié)果
List<int> result = new List<int>(6);
Random random = new Random();
for (int i = 1; i <= 33; i++)
{
container.Add(i);
}
int index = 0;
int value = 0;
for (int i = 1; i <= 6; i++)
{
//從[1,container.Count + 1)中取一個隨機值,保證這個值不會超過container的元素個數(shù)
index = random.Next(1, container.Count + 1);
//以隨機生成的值作為索引取container中的值
value = container[index];
//將隨機取得值的放到結(jié)果集合中
result.Add(value);
//從容器集合中刪除這個值,這樣會導致container.Count發(fā)生變化
container.RemoveAt(index);
//注意這一句與上面一句能達到同樣效果,但是沒有上面一句快
//container.Remove(value);
}
//result.Sort();排序
return result;
}
經(jīng)過這么一改動,確實能做到每次循環(huán)都能生成一個唯一的有效的不重復的數(shù),這樣一來就能做到在M個數(shù)中選取N個數(shù)時只需要循環(huán)M×N次就可以了(M>N,并且都是正整數(shù))。不過也有人會說,我現(xiàn)在還在維護一個.NET1.1的項目,我這里也有類似的需求,可是在.NET2.0以下版本中是沒有泛型的,你能給我想個辦法嗎?我的答案是可以的。
解題方法三
在這個方法中我們不適用泛型集合,只使用數(shù)組,這樣一來這種做法就可以適用于C、Java、PHP等語言和.NET1.1中了。
思路如下,首先使用一個數(shù)組作為存儲所有可能值的容器集合,然后通過循環(huán)每次生成一個隨機值,這個值將來會作為下標來訪問容器集合中的數(shù)值。因為數(shù)組是不可變集合,我們不能將已經(jīng)使用數(shù)值從數(shù)組中刪除,并且它們是簡單的數(shù)據(jù)類型我們不可能給每個數(shù)值增加一個屬性表示數(shù)值是否已經(jīng)被使用過了,那該怎么辦呢?辦法就是每次從可用的下標集合中隨機生成一個值,然后以這個值作為索引從容器集合中得到相應(yīng)的值保存到結(jié)果集合中,除此之外再將這個已經(jīng)使用過的值與數(shù)組中最后一個沒有使用到的值互換位置,然后下一輪再在所有沒有使用過的值中重新再取一個值。代碼如下:
public int[] GenerateNumber3()
{
//用于存放1到33這33個數(shù)
int[] container = new int[33];
//用于保存返回結(jié)果
int[] result = new int[6];
Random random = new Random();
for (int i = 1; i <= 33; i++)
{
container[i - 1] = i;
}
int index = 0;
int value = 0;
for (int i = 0; i < 6; i++)
{
//從[1,container.Count + 1)中取一個隨機值,保證這個值不會超過container的元素個數(shù)
index = random.Next(1, container.Length-1-i);
//以隨機生成的值作為索引取container中的值
value = container[index];
//將隨機取得值的放到結(jié)果集合中
result[i]=value;
//將剛剛使用到的從容器集合中移到末尾去
container[index] = container[container.Length - i-1];
//將隊列對應(yīng)的值移到隊列中
container[container.Length - i-1] = value;
}
//result.Sort();排序
return result;
}
這樣一來,問題得到了解決了。這種做法也可以移植到不支持泛型的版本或者語言當中。
再來一點變動
上面我們處理的都是連續(xù)的情況,假如萬一讓我們在不連續(xù)的集合中隨機選擇5個不重復的,比如在某個班50個學生中隨機抽取5個學生來,貌似上面的做法不行了?其實不然,依然可以沿用這種思路,比如使用上面的第三種辦法即GenerateNumber3(),無非就是聲明一個字符串數(shù)組,在這個字符串數(shù)組中存放全班所有同學的姓名,然后按照下標來隨機取5個姓名即可(具體代碼這里省略)。
不過對這種情況還有不同的,比如在快女和春晚中都有短信投票的環(huán)節(jié),最后會從所有發(fā)送短信的手機號中隨機抽取幾個手機號碼作為中獎號碼(為了簡化,將一號多投的情況視作一次,并且假設(shè)沒有廉租房“連號”的“公平”情況),可能有人會想到照搬上面的情況。實際上在這種情況下不適合,有可能隨機數(shù)的集合相當大,這樣就不適合在內(nèi)存中存儲了,可以考慮使用數(shù)據(jù)庫存儲然后利用數(shù)據(jù)庫的隨機函數(shù),在不同的數(shù)據(jù)庫中取隨機記錄的函數(shù)可能會不同。比如在MySQL中如下(假設(shè)表名為table_name):
select * from `table_name` order by rand() limit 0,10
而在SQL Server中會是如下(假設(shè)表名為table_name):
select top 10 * from [table_name] order by newid()
至于在Oracle中如何隨機抽取記錄,大家google一下吧。
當然,這個情況還可以再復雜一下,可能電視臺針對短信參與的用戶要分一二三等獎,一等獎1名,二等獎2名,三等獎3名,紀念獎50名,那么情況就會稍微再復雜一點,不過也就是再增加一個字段而已,這個字段表示當前的號碼是否已經(jīng)中過獎,每次選取號碼時只選擇那些未曾中獎過的號碼即可。
總結(jié)
關(guān)于不重復隨機數(shù)生成的問題我在七八年前就遇到過,四五年前的時候曾經(jīng)做過總結(jié),最近看到有人在討論這個問題,于是就又重新?lián)炱疬@個話題了。現(xiàn)在撿起這個話題的目的不是想再簡單介紹可能的幾種算法,而是從思路上去說明,并且將情況慢慢復雜化,想要說明的是程序員們(不限于.NET程序員)不要用固定的思路去解決問題,可能同樣的要求在不同的場合下會有不同的做法。明白了思路才能真正做到以不變應(yīng)萬變,學會一兩個控件的用法或者多指導一兩個API并不算什么本領(lǐng),能夠在遇到以前沒有碰到過的問題時迅速簡化解決思路才是本領(lǐng),另一種本領(lǐng)就是遇到錯誤時如何快速根據(jù)經(jīng)驗定位錯誤產(chǎn)生原因的本領(lǐng)。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉(zhuǎn)載自:慧都控件網(wǎng)