非重复随机序列生成算法

最近编程时遇到一个问题:有一组对象,要求随机地访问其中每一个对象,并且每个对象只访问一次。如果我们将访问顺序转换为一组整数序列,那么这就是一个关于“非重复随机序列生成算法”的问题。

本文将探讨这个问题的多种解法,并给出一个非常高效的算法。

 

【问题描述】:有一个自然数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也可以理解为一种随机排序算法,可以应用在很多场合,例如:洗牌、抽签等。

 

    原文作者:卡卡西村长
    原文地址: https://www.cnblogs.com/lavezhang/archive/2012/05/14/2498981.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞