非重複隨機序列生成算法

最近編程時遇到一個問題:有一組對象,要求隨機地訪問其中每一個對象,並且每個對象只訪問一次。如果我們將訪問順序轉換爲一組整數序列,那麼這就是一個關於“非重複隨機序列生成算法”的問題。

本文將探討這個問題的多種解法,並給出一個非常高效的算法。

 

【問題描述】:有一個自然數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/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

發佈了33 篇原創文章 · 獲贊 31 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章