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