最近編程時遇到一個問題:有一組對象,要求隨機地訪問其中每一個對象,並且每個對象只訪問一次。如果我們將訪問順序轉換爲一組整數序列,那麼這就是一個關於“非重複隨機序列生成算法”的問題。
本文將探討這個問題的多種解法,並給出一個非常高效的算法。
【問題描述】:有一個自然數N,希望得到一個整型序列,該序列包含N個整數,從0到N-1,呈隨機分佈狀態,且不重複。
【問題分析】:生成隨機數是簡單的,關鍵是,如何保證不重複呢?一般來說,我們有兩種思路:
思路1:我們不能保證每次生成的隨機數都是不重複的,但是可以在生成隨機數之後,判斷這個數值是否已經生成過了,如果已經生成過了,那就重新生成一個,直到生成一個新的數值。
思路2:每生成一個隨機數之後,就調整隨機數的生成規則,將已經生成過的數值排除掉,從而保證每次生成的隨機數一定是新的。
接下來,我們將根據這兩種思路,給出幾種算法,並分析每種算法的複雜度。
【算法1】
“思路1”是絕大多數程序員的思維。如果在項目開發過程中遇到這個問題,我相信大部分程序員一定會這麼做:創建一個List,用於保存隨機生成的數值,每次插入時,先判斷這個數值是否已經在List中存在了,如果已經存在,則重新生成。C#代碼如下:
/// <summary> /// 生成一個非重複的隨機序列。 /// </summary> /// <param name="count">序列長度。</param> /// <returns>序列。</returns> private static List<int> BuildRandomSequence1(int length) { List<int> list = new List<int>(); int num = 0; for (int i = 0; i < length; i++) { do { num = random.Next(0, length); } while (list.Contains(num)); list.Add(num); } return list; }
上述算法簡單易懂,時間複雜度是O(N²) ,空間複雜度是O(N)。該算法的侷限在於,每生成一個隨機數,都要遍歷List,判斷是否已經存在,這就導致時間複雜度太高。那麼,如何改善這個算法呢?請繼續往下看。
【算法2】
我們知道,遍歷List的時間複雜度是O(N),訪問Hashtable的時間複雜度是O(1)。所以,如果我們使用Hashtable來判斷是否重複,就可以將整個算法的時間複雜度從O(N²)降至O(N),而空間複雜度仍然基本保持在O(N)。
C#代碼如下:
/// <summary> /// 生成一個非重複的隨機序列。 /// </summary> /// <param name="count">序列長度。</param> /// <returns>序列。</returns> private static Hashtable BuildRandomSequence2(int length) { Hashtable tab = new Hashtable(length); int num = 0; for (int i = 0; i < length; i++) { do { num = random.Next(0, length); } while (tab.Contains(num)); tab.Add(num, null); } return tab; }
經過測試,性能確實有了非常大的提升。但是,有一個問題依然存在:隨着Hashtable中的數值越來越多,重複概率也會越來越高,這一點很容易理解。如何才能降低重複概率、進一步提高算法性能呢?不妨嘗試一下“思路2”。
【算法3】
“思路2”的基本思想是:利用隨機數的生成特點,將已經生成的數值,排除在隨機區間之外,這樣就可以確保下次生成的隨機數一定是新的。具體來說,我們可以這樣做:
首先,建立一個長度爲N的數組array,初始值是0…N-1。
然後,生成一個隨機數x1=random.Next(0, N),則x1∈[0,N)。取num1=array[x1]作爲序列中的第一個成員。接下來是關鍵步驟:將num1和array[N-1]交換。
然後,生成下一個隨機數x2= random.Next(0, N-1),則x2∈[0,N-1)。由於num1已經被交換到了array[N-1],而x2<N-1,所以num2=array[x2]一定不等於num1,從而避免了重複。然後將num2和array[N-2]交換。
按照上述方法,可以得到序列中第三、第四…第N個成員。最後得到的array就是一個非重複的隨機序列。以下是整個計算過程的圖形演示(假設N=5):
C#代碼如下:
/// <summary> /// 生成一個非重複的隨機序列。 /// </summary> /// <param name="count">序列長度。</param> /// <returns>序列。</returns> private static int[] BuildRandomSequence3(int length) { int[] array = new int[length]; for (int i = 0; i < array.Length; i++) { array[i] = i; } int x = 0, tmp = 0; for (int i = array.Length - 1; i > 0; i--) { x = random.Next(0, i + 1); tmp = array[i]; array[i] = array[x]; array[x] = tmp; } return array; }
經過分析,算法3的時間和空間複雜度都是O(N),性能非常高。通過巧妙的交換位置的方法,可以確保每次得到的數值一定是不重複的,所以不用去判斷是否重複。而且,使用數組來保存序列,比List和Hashtable性能更好。
上述算法生成的隨機序列是從0到N-1,如果我們指定了別的區間範圍呢?例如,要求生成一個非重複的隨機序列,範圍是[low, high]。實現起來非常簡單,只要把算法3稍微改一下就可以了。C#代碼如下:
/// <summary> /// 生成一個非重複的隨機序列。 /// </summary> /// <param name="low">序列最小值。</param> /// <param name="high">序列最大值。</param> /// <returns>序列。</returns> private static int[] BuildRandomSequence4(int low, int high) { int x = 0, tmp = 0; if (low > high) { tmp = low; low = high; high = tmp; } int[] array = new int[high - low + 1]; for (int i = low; i <= high; i++) { array[i - low] = i; } for (int i = array.Length - 1; i > 0; i--) { x = random.Next(0, i + 1); tmp = array[i]; array[i] = array[x]; array[x] = tmp; } return array; }
爲了驗證上述三種算法的實際性能,我們以生成隨機序列所耗的平均時間爲標準,進行了實際測試。
測試環境爲:Windows7/ CPU 1.6GHZ /VS2008/C#。測試結果爲:
序列長度 |
算法1 |
算法2 |
算法3 |
100 |
15ms |
<1ms |
<1ms |
1000 |
46ms |
<1ms |
<1ms |
10000 |
4430ms |
31ms |
<1ms |
【總結】
本文算法3的關鍵方法是:在現有數組基礎上,通過不斷地交換位置,來巧妙地達到時間和空間的最優。
其實,一些經典排序算法採用的也是這個思想,例如:冒泡排序、快速排序、堆排序,等等。
算法3也可以理解爲一種隨機排序算法,可以應用在很多場合,例如:洗牌、抽籤等。
作者:Lave Zhang
出處:http://www.cnblogs.com/lavezhang/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。