最近編程時遇到一個問題:有一組對象,要求隨機地訪問其中每一個對象,並且每個對象只訪問一次。如果我們將訪問順序轉換為一組整數序列,那麼這就是一個關於“非重複隨機序列產生演算法”的問題。
本文將探討這個問題的多種解法,並給出一個非常高效的演算法。
【問題描述】:有一個自然數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也可以理解為一種隨機排序演算法,可以應用在很多場合,例如:洗牌、抽籤等。