• 非重复随机序列生成算法


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

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

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

  • 相关阅读:
    Spark2.x学习笔记:Spark SQL程序设计
    Spark2.x学习笔记:Spark SQL的SQL
    Spark2.x学习笔记:Spark SQL快速入门
    Hystrix入门执行过程
    redis配置master-slave模式
    MockServer 入门
    Locust分布式负载测试工具入门
    poi读取excel元素
    hadoop+spark集群搭建入门
    Vi文档
  • 原文地址:https://www.cnblogs.com/lavezhang/p/2498981.html
Copyright © 2020-2023  润新知