我想生成N个不同随机数的列表:

public static List<int> GetRandomNumbers(int count)
{
    List<int> randomNumbers = new List<int>(); 

    for (int i=0; i<count; i++) 
    {    
        int number;

        do number = random.Next();
        while (randomNumbers.Contains(number));

        randomNumbers.Add(number);
    }

    return randomNumbers;
}


但是我觉得有更好的方法。这个do...while循环特别难看。关于如何改善这一点的任何建议?

评论

@MartinR不完全是。 Fisher–Yates随机播放可让您对给定范围内的所有元素进行排列。

@BrunoCosta:“因此,如果数字不重复,您实际上就不能再称它为随机数,它将具有不重复的特性,这是一个非常独特的特性”:事实并非如此。 “随机”并不意味着“独立”,更不用说“没有任何有趣的特性”。哎呀,根据您对“ random”的定义,我们甚至不能说“ random integer”,因为作为整数的性质意味着它不是随机的。 。 。

鲍勃·弗洛伊德(Bob Floyd)为此目的发明了一种算法。请参阅:stackoverflow.com/a/2394292/179910。它比您显示的内容或当前发布的任何答案要好得多。

@JerryCoffin,我相信我是正确的,例如,结果中的第一个值永远不能为N,这意味着不会保持结果的随机性。可以保持选择的随机性,但是顺序不是随机的(足够多)。我们可以在第二台显示器中清除它吗?

看来这家伙很好地解决了您的问题。这就是他在帖子的第一行中所说的:在本文中,我将展示一种制作迭代器的方法,该迭代器将以随机顺序访问列表中的项目,仅访问每个项目一次,并告诉您何时它访问了所有项目并完成。它无需存储随机列表即可做到这一点,也不必跟踪已访问过的项目。

#1 楼


针对悬赏金的最新答案:请参阅这是您的最终答案吗?最后,还有其他更改-基本上重写了答案。



将问题分解为要求:


您需要一组随机数
数字必须是唯一的
返回数字的顺序必须是随机的

您当前的代码表明随机数的范围由Random.Next()指定,该函数返回[0 .. Int32.MaxValue)范围内的值(注意,不包括Int32.MaxValue)。这对于这个问题而言意义重大,因为其他答案都假定范围是可配置的,并且范围很小。

如果范围是可配置的,那么推荐的算法可能会更大。

基于这些假设,让我们进行代码审查...

代码样式

做...而

这里最明显的问题是无支撑的do-while循环。您已经知道了,但是这段代码很丑陋:
    do number = random.Next();
    while (randomNumbers.Contains(number));


这样可以使语句清晰明了,并大大减少了混乱。始终对1衬使用括号。

列表构造

List类允许使用初始容量。由于容量仅需count,因此使用此容量初始化列表是有意义的:可以做出最有趣的观察。让我们分析一下您当前的算法:


为结果创建一个容器
选择一个随机值
检查它是否曾经被选中
如果它是'new',然后将其添加到容器中



该算法将在

随机顺序,并且具有良好的随机特性(无偏斜,偏差,间隙等)。换句话说,您的结果很好。

问题在于性能....

这里有两个性能问题,一个很小,另一个很大:


do-while循环可避免发生冲突
列表容器

do-while性能

do-while具有对性能的影响很小...几乎可以忽略不计。这是一个激烈争论的话题,但是现实是,在这成为问题之前,您将需要非常大的count。原因如下:

先前选择随机值时发生冲突。对于[0 .. Int32.MaxValue)的指定范围,在实际发生碰撞之前,您将需要非常大的count。例如,count必须大约为65,000,才有可能发生甚至一次碰撞的可能性要大于50%。选择\ $ M \ $数字。如果\ $ M <\ sqrt {N} \ $,则单次碰撞的可能性小于50%。由于范围很大,因此概率很小。

显然,如果范围很小,则概率将受到显着影响。但是范围固定在Int32.MaxValue,所以可以。

另外,如果count很大,那么概率也将受到影响。多少会很大?好吧,在遇到重大问题之前,您将遇到非常大的阵列.....我敢冒险在运行到性能上的一个重要问题。

列出性能
randomNumbers.Contains(number)调用来确定以前是否选择了一个值。这需要扫描所有先前选择的值才能确定。如前所述,这几乎总是返回false,因此必须扫描整个列表。

随着count值的增加,执行Contains的时间长度将以\ $ O(n ^ 2)\ $的平方速率增加,其中ncount。比随机冲突问题要早得多。

将其放在一起

您的代码中的问题是您尝试一次执行太多操作,一个List,因为这是您的返回值,而HashSet会更好。如果您将问题分解为多个阶段,则可以更轻松地解决问题。

如果将重复值添加到HashSet中,则它不会增长,操作性能也不依赖于此。 HashSet中的数据量(它是\ $ O(1)\ $)。您可以使用HashSet的Count来管理数据的唯一性。

一旦有了干净的唯一随机数集,就可以将它们转储到List中,然后使用高效的shuffle来对列表进行shuffle 。

以正确的方式组合这些数据结构会导致一个整体的\ $ O(n)\ $解决方案,该解决方案将很好地扩展。代码,也可以在Ideone中使用。请注意,我的C#较弱,因此我试图使逻辑更清楚。第二个备用算法

上述算法的问题有三方面:


当count很大时,冲突的可能性会增加,并且性能可能会受到影响。
在某个时候,HashSet和List中的数据都需要同时存在,因此空间使用量将增加一倍。
最后需要进行随机排序以将数据保持在随机顺序中(HashSet不会将数据保持在任何特定顺序中,并且哈希算法将导致该顺序变得有偏差并倾斜)。

这些只是数量很大时的性能问题。请注意,只有大量冲突会影响解决方案的可伸缩性(从很大程度上来看,冲突不再是\ $ O(n)\ $,并且当计数接近Int32.MaxValue时,冲突会逐渐恶化。

@JerryCoffin指出了鲍勃·弗洛伊德(Bob Floyd)的另一种算法,其中使用了一种技巧以确保不会发生碰撞。这解决了非常多的可伸缩性问题,无法解决对HashSet和List的需求,也无法解决对shuffle的需求。


    do
    {
        number = random.Next();
    } while (randomNumbers.Contains(number));



假设RandInt(1, J)返回包含J的值。

要了解上述算法,您需要认识到您从一个小于整个范围的范围中选择一个随机值,然后在每个值之后将其扩展为包括一个以上的值。发生碰撞时,您可以放心地插入最大值,因为以前不可能包含它。

碰撞的可能性以与值的数量减少相同的速率增加,因此结果中任何一个数字的概率都不会偏斜或有偏差。

这几乎是最终答案吗?否

因此,使用上述解决方案,在C#中,看起来像(在Ideone中)(注意,代码现在与Ideone不同):

List<int> randomNumbers = new List<int>(count);


请注意,您仍需要重新整理结果,以便解决HashSet问题。另请注意,需要执行奇特的循环条件top > 0,因为在溢出时,事情会变得凌乱。

可以避免随机播放吗?

因此,这解决了做碰撞循环,但是,洗牌呢?可以通过同时维护HashSet和List来解决。没有!考虑此功能(也适用于Ideone):

using System;
using System.Collections.Generic;

public class Test
{
    static Random random = new Random();

    public static List<int> GenerateRandom(int count)
    {
        // generate count random values.
        HashSet<int> candidates = new HashSet<int>();
        while (candidates.Count < count)
        {
            // May strike a duplicate.
            candidates.Add(random.Next());
        }

        // load them in to a list.
        List<int> result = new List<int>();
        result.AddRange(candidates);

        // shuffle the results:
        int i = result.Count;  
        while (i > 1)
        {  
            i--;  
            int k = random.Next(i + 1);  
            int value = result[k];  
            result[k] = result[i];  
            result[i] = value;  
        }  
        return result;
    }
    public static void Main()
    {
        List<int> vals = GenerateRandom(10);
        Console.WriteLine("Result: " + vals.Count);
        vals.ForEach(Console.WriteLine);
    }
}


上面的答案永远不会允许结果中的第一个值具有Max - CountMax的任何值(因为第一个值永远不会发生冲突,并且范围不会

即使使用这种避免冲突的算法,您仍然需要重新整理结果以确保数字的明确偏差。


TL; DR

这是您的最终答案吗?是的!

已经使用了很多代码,很明显拥有基于范围的输入以及Int32.MaxValue系统非常有用。大范围的混乱也会导致32位整数空间中的潜在溢出。

使用@mjolka,以下代码将是两全其美的方法:

initialize set S to empty
for J := N-M + 1 to N do
    T := RandInt(1, J)
    if T is not in S then
        insert T in S
    else
        insert J in S


评论


\ $ \ begingroup \ $
为什么要洗牌?我对此感到困惑,因为每个人似乎都将其包含在答案中,但操作员不在问题中
\ $ \ endgroup \ $
–IEatBagels
14年8月28日在19:27

\ $ \ begingroup \ $
@TopinFrassi:因为rolfi下达了数字(以检查重复项),但不应下达它们。
\ $ \ endgroup \ $
–查尔斯
2014年8月28日在20:05

\ $ \ begingroup \ $
@TopinFrassi是正确的,他应该只返回候选人,这样就可以了。删除混排部分仅仅比op的回答好一点,因为它使用哈希集,使查找重复项的成本降低。我说过,这是一个关于改组的问题,但是当然有很多实现都可以满足要求。
\ $ \ endgroup \ $
–布鲁诺·科斯塔(Bruno Costa)
2014年8月28日在22:41



\ $ \ begingroup \ $
@BrunoCosta-HashSet将对列表中的数据施加非随机(尽管是非结构化)顺序。 OP的代码将对结果产生随机顺序。需要随机播放以保留随机有序输出。至于“稍微好一点”的部分,HashSet将比List快N / 4倍(其中N是计数),因此,对于较大的N(例如10,000个计数),HashSet将快2500倍左右....我想这可能是“轻微的”。
\ $ \ endgroup \ $
–rolfl
2014年8月28日在22:48

\ $ \ begingroup \ $
我喜欢这个答案,付出了很多努力+1。我担心看不到决赛,在途中填满了名单。但是仍然有一个异议:最终解决方案的随机性并不完美,一开始就永远不会允许最高价值(洗牌有助于避免这种情况)。我个人将使用HashSet修补的原始版本进行测试。没有更多更改(代码样式除外)。当然,我认为不希望通过2G int-> 8GB的列表来占用内存。
\ $ \ endgroup \ $
–user52292
2014年9月4日在17:49

#2 楼

是的,肯定有。

您将生成一个元素集合,将其融合在一起并开始从中拉出项目。一个快速的班轮是:

Enumerable.Range(0,100).OrderBy(x => Guid.NewGuid()).Take(20);


或替代地

Enumerable.Range(0,100).OrderBy(x => random.Next()).Take(20);


这将给您20个唯一的随机值从0到100的范围。

与您的方法的不同之处在于,您遇到最坏的无穷大场景:如果您不幸地不幸并最终不断获得相同的值怎么办?您将永远无法获得所需数量的随机值。

另一方面,我的方法将首先生成100个值,然后从中获取一个子集,您可以批评它对内存的影响。考虑到您使用的random.Next()使用的是整数范围的一半,因此实际上您应该警惕,因为它将对内存产生巨大的影响。

这还取决于您的具体情况:如果您有一个很大的值池(1.000.000),并且需要2个随机值,那么您的方法会更好。但是,当您需要从同一个池中获得999.999个值时,我的方法仍然值得商still。

生成这些最后的值将需要一些时间;您可以通过以下方法为自己进行大量测试:

void Main()
{
    var random = new Random();
    var times = new TimeSpan[512];
    var values = new bool[512];
    var sw = new Stopwatch();

    for(int i = 0; i < times.Length; i++) 
    {   
        sw.Restart();
        while(true) {
            int rand = random.Next();

            if(rand > 7894500 && rand < 7894512) 
            {
                int index = rand - 7894500;
                if(!values[index])
                {
                    values[index] = true;
                    break;
                }
            }
        }
        sw.Stop();
        times[i] = sw.Elapsed;
        Console.WriteLine ("Elapsed time: " + sw.Elapsed);
    }

    var orderedTime = times.OrderBy(x => x);
    for(int i = 0; i < 512; i++)
    {
        Console.WriteLine (orderedTime.ElementAt(i));
    }
}


它将继续在您的值列表中随机循环512次,并在找到(由我自己随机选择)的值介于78945007894512之间。之后,该值被视为已访问以正确模拟现实(在较早的版本中,所有512转具有512个可用值)。执行此操作时,您会发现查找上一个值会花费很多时间(有时速度很快,需要39毫秒,而其他时间则需要一分钟以上)。显然,开始时快,而结束时慢。

相比之下,我的方法的内存开销将首先分配3200万个整数,引导,填充,对象开销等,而您的内存不足。

您也许可以通过使用更“真实”的改组算法来改善它,该算法没有对象和向导开销。如果人口总数为3200万+ 1,您将不得不承受大量的内存开销或巨大的执行时间开销。


我可以搁置该主题之前的最后一个编辑:我在聊天中使用@rolfl进行了讨论,我们得出的结论是,我们的任何一种解决方案都具有用法,但这取决于您的实际情况。总结起来会是这样的:

如果您有很大范围的值(例如0到int.MaxValue),那么我的解决方案将占用您PC上的所有内存。您正在查看两个集合,每个集合具有21亿个整数,然后从中进行分割。

在我的解决方案中,您首先分配整个总体,然后对其进行排序(不同的数据结构),然后拿一些。如果这个“数目”接近21亿,您将在不使用数据的情况下付出巨大的代价。

这与@rolfl的答案相比如何?他基本上是根据需要分配数据:如果他需要3200万个值,那么他只会分配这3200万(x2)而不是更多。如果他需要21亿,那么他最终将像我一样遇到内存不足的情况,但这是一个不常见的最坏情况,而对我来说这是标准行为。

他的方法的主要缺点是您想要的值的数量达到总人口,将很难获得那些最后的值,因为会有很多冲突。再说一次,这只有在人口真的很大的情况下才是问题。

那么什么时候应该使用我的解决方案?像大多数事物一样,在可读性和性能之间要进行权衡。我认为,如果您的人口相对较少且数据集较大,那么可读性就会对性能造成影响。而且,当总体相对较小且我们想要的值数量接近时,我的解决方案将具有与其他方法相似的内存影响,但同时也避免了很多冲突。

根据您的情况选择。

评论


\ $ \ begingroup \ $
-1,因为此代码与OP的要求不兼容,并且可能无法在较大范围(Int的MIN到MAX值)范围内成功完成。
\ $ \ endgroup \ $
–rolfl
2014年8月28日14:30在

\ $ \ begingroup \ $
如果您需要1,000,000中的999,999个值,那么您的代码将需要大约16MB的空间来仅存储GUID(假设它存储在一个完整的16字节值中,而不是字符串或其他形式),加上我确定,另外4MB用于int值,再加上对象开销,然后还有更多。
\ $ \ endgroup \ $
–rolfl
2014年8月28日14:35

\ $ \ begingroup \ $
此外,未将GUID指定为随机的,并且按GUID排序不会产生您期望的结果
\ $ \ endgroup \ $
–rolfl
2014年8月28日14:39

\ $ \ begingroup \ $
我认为您对OP的代码陷入无限循环的担心并不成立。对于所有种子值,C#使用的PRNG的AIUI保证保证> 2 ^ {56},这意味着,只要count是一个合理的值,它就应该始终产生有效的结果。实际上,在这成为问题之前,您将耗尽内存来存储生成的值。
\ $ \ endgroup \ $
–法律
2014年8月30日在2:33



\ $ \ begingroup \ $
排序功能不是混洗功能。当您可以使用耗费O(1)和O(N)时间的Fischer-Yates随机播放时,为什么还要为密钥生成O(N)个新内存并花O(N log N)时间对其进行排序?
\ $ \ endgroup \ $
–安德鲁(Andrew)
2014年8月30日12:53

#3 楼

代替使用List<int>,您应该使用HashSet<int>HashSet<>禁止使用多个相同的值。然后Add方法返回一个bool,指示是否将元素添加到列表中,这样您就可以更改以下代码:

public static List<int> GetRandomNumbers(int count)
{
    List<int> randomNumbers = new List<int>(); 

    for (int i=0; i<count; i++) 
    {    
        int number;

        do number = random.Next();
        while (randomNumbers.Contains(number));

        randomNumbers.Add(number);
    }

    return randomNumbers;
}


请注意,我将返回值从List<int>更改为IEnumerable<int>,如果您不打算从列表中添加/删除值,则应该返回IEnumerable<int>,如果您打算这样做它,返回ICollection<int>。您不应该返回List<int>,因为它是实现细节,并且List<>并不是可扩展的。

评论


\ $ \ begingroup \ $
使用Set的问题在于它没有顺序。没有办法使整数按随机顺序排列。
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年8月28日14:30在

\ $ \ begingroup \ $
@SimonAndréForsberg行动党未指定他想这样做,除非我失踪了
\ $ \ endgroup \ $
–IEatBagels
2014年8月28日14:34



\ $ \ begingroup \ $
实际上,您是正确的。我完全误解了这个问题。
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年8月28日下午14:45

\ $ \ begingroup \ $
发生在每个人身上!
\ $ \ endgroup \ $
–IEatBagels
2014年8月28日下午14:45

\ $ \ begingroup \ $
你不应该在那里打电话给ToList。除此之外,这是一个很好的解决方案
\ $ \ endgroup \ $
–布鲁诺·科斯塔(Bruno Costa)
2014年8月28日23:39



#4 楼

扩展我的评论。这称为线性同余生成器。我使用了通用参数,这些参数我认为是最低标准。可以选择其他参数,但这是一项棘手的任务。该序列从种子开始,到达0到M-1之间的所有其他数字,然后在再次到达种子时重新开始。这是伪随机的。 507111939始终跟随285719。由于该序列达到了所有M个数字,因此该序列的M个连续输出中都将没有重复。

代码:


用法:

class RandomSequence {

  int actual;
  static final int M = (1<<31) -1;

  public RandomSequence(int seed) {
    this.actual = seed
  }

  public int next() {
    return this.actual = (16807 * this.actual) % RandomSequence.M
  }

}


评论


\ $ \ begingroup \ $
这是一个很好的解决方案。您应该注意,这项工作相当有效,因为计算理论中有许多方便的巧合,例如M是\ $ 2 ^ {31}-1 \ $,它也是Integer.Max_value,它也恰好是质数。素有重要意义。 16807也是素数。
\ $ \ endgroup \ $
–rolfl
2014年8月30日12:49



\ $ \ begingroup \ $
你能解释一下你在说什么吗
\ $ \ endgroup \ $
–穆罕默德·乌默尔
15年7月24日在4:47

\ $ \ begingroup \ $
我在说@rolfl
\ $ \ endgroup \ $
–穆罕默德·乌默尔
2015年8月1日,下午3:12

#5 楼

该算法对我来说看起来不错。它没有明显的缺陷,并且在常见的用例中都可以正常工作。但是,除非这是一个预期的用例,否则我不会费心将代码更改为更复杂的方法。 >
do
{
    bar();
}
while (foo);


我还将方法重命名为GetNRandomNumbers,以使该方法的名称更符合其用途。

评论


\ $ \ begingroup \ $
+1。我通常不喜欢在块周围放置括号,但我认为在do ... while循环中,它们特别重要。起初,我由于缺少花括号而误读了代码。
\ $ \ endgroup \ $
–法律
2014年8月30日在2:37

\ $ \ begingroup \ $
风格不是问题,对吗?
\ $ \ endgroup \ $
– Florian F
2014年9月4日在16:43

\ $ \ begingroup \ $
@FlorianF不,不是。但是答案中的第一句话说算法对我来说看起来不错,不是吗?然后我做了一些建议,以提高错误的适应性(使用括号)和清晰度(重新命名)。您可以尝试更清楚自己的意思吗?
\ $ \ endgroup \ $
– ANeves认为SE是邪恶的
2014年9月4日在17:43

\ $ \ begingroup \ $
@FlorianF样式始终是代码检查的有效目标。
\ $ \ endgroup \ $
– David Harkness
2014年9月4日在18:35

\ $ \ begingroup \ $
@FlorianF,这是代码审查,而不是堆栈溢出。审阅者始终可以对代码的任何方面发表评论。总是。
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
2014年9月4日23:44

#6 楼

另一种解决方案是使用格式保留加密密码(例如使用FFX模式的AES),该密码配置为从32位整数映射到32位整数。计数”整数。

这些加密的数字将与种子一样随机,并且不会重复。

评论


\ $ \ begingroup \ $
有趣的想法,并且可能比OP和rolfl的代码快得多,因为它们可能是O(n)而不是O(n ^ 2)或O(n ln n)。 Jeroen Vannevel的解决方案也是O(n),并且可能还会更快,但如果所需的随机数范围太大,则无法合理使用。该解决方案可能具有最低的存储要求,因为可以按需生成数字,而不必事先计算。
\ $ \ endgroup \ $
–法律
2014年8月30日在2:41



#7 楼

我正在更新我的答案,以便对BobFloyd的算法进行分析,并总结我可以发现的所有缺陷。其中一些是我以前的回答,一些用户评论(例如@rolfl和@firda)中也提到了它们。请仔细阅读整个答案,因为最后您会感到惊讶。

让我定义一些我将在答案中使用的术语。考虑方法定义:

static IEnumerable<int> NonRepeatingRandomSequence(int min, int max, int count, int? seed = null)


所采用的元素=计数

第一个问题:最后一个range length - taken elements元素永远都不能位于结果的索引0中。当它们相同时,这更容易看清,但是当它们接近时,结果根本不是随机的。

第三期:还有第三期,但我只会在结尾处提及我的回答。

我实现了三种用于返回非重复数字的算法:

纯混洗算法,纯Bob Floyd算法和带混洗的Bob Floyd算法。这些将在以后进行比较和测试时出现。

我还实施了一种测试,以通过任何算法检查输出列表的随机性。尽管它有一些限制,但为了回答完整性/正确性就足够了。对于很多错误,这可能是我对随机性的定义。在这里,我基本上是在计算每种组合发生的次数,并查看平均值是否在可接受的范围内。

改组算法,这是我的第一个算法:

public void TestRandomness(Func<List<int>> getRandomList, int rangeLength, int count)
{
    long combinations = (long)(rangeLength.Factorial(rangeLength - count + 1));
    long iterations = combinations * 100;
    var partitioner = Partitioner.Create(0, iterations, iterations / 4);
    ConcurrentDictionary<long, int> ocurrences = new ConcurrentDictionary<long, int>(Environment.ProcessorCount, (int)combinations);

    Parallel.ForEach(partitioner, new ParallelOptions() {MaxDegreeOfParallelism = Environment.ProcessorCount},
        range =>
        {
            //hopefully having a private dictionary will help concurrency
            Dictionary<long, int> privateOcurrences = new Dictionary<long, int>();
            for (long i = range.Item1; i < range.Item2; ++i)
            {
                var list = getRandomList();
                long acc = 0;
                //this will only work when numbers are between 0 and 88
                foreach (var value in list)
                {
                    acc = (value + 11) + acc*100;
                }
                privateOcurrences.AddOrUpdate(acc, 1, v => v + 1);
            }
            foreach (var privateOcurrence in privateOcurrences)
            {
                ocurrences.AddOrUpdate(privateOcurrence.Key, 
                    privateOcurrence.Value,
                    (k, v) => v + privateOcurrence.Value);
            }
        });

    double averageOcurrences = iterations / combinations;
    double currentAverage = ocurrences.Values.Average();
    Debug.WriteLine("The average ocurrences of this implementation is {0} comparing to {1} in the 'perfect' scenario", currentAverage, averageOcurrences);
    Assert.Less(currentAverage, averageOcurrences * 1.05);
    Assert.Greater(currentAverage, averageOcurrences * 0.95);
}


通过,通过,通过。该算法符合我对随机性的测试定义。这与@Jeroen Vannevel指出的非常接近,除了不需要随机的部分,它也不需要O(n log n)时间运行(我需要引用@Andrew Piliser,因为他是唯一要评论的人对这个)。

我们知道该算法将占用相当于O(范围长度)的内存,这是他的主要缺点。但是,它不会产生冲突,并且可以保证随机性。

现在该用Bob Floyd算法证明第一和第二个问题了:

[Test]
public void TestShuffleRandomness()
{
    TestRandomness(() => Enumerable.Range(0, 16).ToList().Shuffle().Take(4).ToList(), 16, 4);    
}


太好了,现在我们证明了鲍勃·弗洛伊德(Bob Floyd)算法本身并不能解决此问题,并且当然不会通过我的随机性测试。

public static IEnumerable<int> BobFloydNonRepeatingSequence(int min, int max, int count, int? seed = null)
{
    Random random;
    if (seed != null)
    {
        random = new Random(seed.Value);
    }
    else
    {
        random = new Random();
    }
    long length = max - min + 1;
    INonRepeatingList values = NonRepeatingListFactory.GetNonRepeatingList(min, max, count);
    for (int i = (int)(length - count); i < length; ++i)
    {
        if (!values.Add(random.Next(min, i+min+1)))
        {
            values.Add(i+min);
        }
    }
    return values;
}

[Test]
public void TestLastCountElementsAreNotInIndex0()
{
    for (int i = 0; i < 1000000; ++i)
    {
        var list = Sequence.BobFloydNonRepeatingSequence(0, 15, 4).ToList();
        Assert.IsFalse(new[] { 12, 13, 14, 15 }.Contains(list[0]));
        Assert.AreEqual(4, list.Count);
    }
}

[Test]
public void TestBobFloydBecomesLinear()
{
    var list = Sequence.BobFloydNonRepeatingSequence(0, 15, 16).ToList();
    for (int i = 0; i < list.Count; ++i)
    {
        Assert.AreEqual(i, list[i]);
    }
}


我们唯一的选择剩下的就是重新整理结果,并希望获得最好的结果。

[Test]
public void TestBobFloydRandomness()
{
    TestRandomness(() => Sequence.BobFloydNonRepeatingSequence(0, 15, 4).ToList(), 16, 4);
}


让我们看看我的随机性测试对此有何评论: />
我希望你不要抱太大希望。我的随机性测试失败,这是鲍勃·弗洛伊德(Bob Floyd)的第三个也是最后一个问题,因为它不是随机的。或至少它不尊重我提供的随机定义。

这只是讨论Bob Floyd算法与rolfl的实现。随机性的定义有点严格,或者鲍勃·弗洛伊德(Bob Floyd)混洗算法对于您的目的来说足够随机。我真的不知道实现中缺少什么,因此结果将更加随机,因此它仍然与效率有关。

如您在我的实现中所见,我从该方法中获得了一个INonRepeatingList taken elements

public static IEnumerable<int> NonRepeatingRandomSequence(int min, int max, int count, int? seed = null)
{
    Random random;
    if (seed != null)
    {
        random = new Random(seed.Value);
    }
    else
    {
        random = new Random();
    }
    long length = max - min + 1;
    INonRepeatingList values = NonRepeatingListFactory.GetNonRepeatingList(min, max, count);
    for (int i = (int)(length - count); i < length; ++i)
    {
        if (!values.Add(random.Next(min, i+min+1)))
        {
            values.Add(i+min);
        }
    }
    values.Shuffle();
    return values;
}


此方法根据占用的内存确定要使用的列表。如果您始终像@rolfl一样使用HashSet,则建议在元素较大且范围长度也较大时占用大量内存。
在这种情况下,您可以将值映射到一点。它可能有多大的不同?每8KB范围长度大约只有1KB内存。
另一方面,如果我们要从多个元素中提取少量元素,则应该使用HashSet。

评论


\ $ \ begingroup \ $
快速浏览后,我注意到我没有安全地使用随机线程,尽管如此,我的测试结果并未改变。
\ $ \ endgroup \ $
–布鲁诺·科斯塔(Bruno Costa)
2014年9月6日下午0:39

\ $ \ begingroup \ $
Bob Floyd算法的翻译中有错误;尝试Sequence.BobFloydNonRepeatingSequence(8,14,3,7)-抛出ArgumentOutOfRangeException。
\ $ \ endgroup \ $
–mjolka
2014年9月7日在8:43

\ $ \ begingroup \ $
@mjolka再次感谢。我相应地更新了我的答案。
\ $ \ endgroup \ $
–布鲁诺·科斯塔(Bruno Costa)
2014年9月7日下午13:19

\ $ \ begingroup \ $
Bob Floyd算法仍然存在错误; values.Add(i + min)应该是values.Add(i + min-1)。
\ $ \ endgroup \ $
–mjolka
2014年9月8日下午0:21

\ $ \ begingroup \ $
@mjolka不,不应该。
\ $ \ endgroup \ $
–布鲁诺·科斯塔(Bruno Costa)
2014年9月8日下午6:12

#8 楼

尽管byr @Jeroen Vannevel的解决方案非常简单,并且可以满足大多数目的和目的,但是如果您只想在很大范围内输入几个数字,您会发现性能特别差。

建议使用以下算法:


从范围内选取一个数字
从其余范围内选取一个数字
,依此类推...


实现看起来像这样:


选择从0到N的数字
选择从0到N-1的数字
依此类推
确定每个图形代表的数字

0到10的示例


绘制4
绘制7
...
结果:4代表4,7代表8,因为它现在是第七个可用数字。


评论


\ $ \ begingroup \ $
您可以添加一个算法示例代码吗?
\ $ \ endgroup \ $
–IEatBagels
2014年8月28日在14:22

\ $ \ begingroup \ $
这会起作用,但是在您随机分配了5、6并获得下一个数字7之后,会提供一些额外的开销。我假设您打算将7修改为9? (因为5和6已经消失)。
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年8月28日14:34

\ $ \ begingroup \ $
@SimonAndréForsberg确实,您必须尝试在开销值得额外努力的情况下尝试,但是当增加范围时,应该有一个转折点,使它变得更有效率。
\ $ \ endgroup \ $
– Dennis Jaheruddin
2014年8月28日14:41

\ $ \ begingroup \ $
我在这里看到了含义,并且误解了您建议的算法。您是正确的,不会有任何偏斜,但是您的答案在如何管理范围上留下了很大的空白,尤其是考虑到范围是整个有效的int 32位空间。您仍然需要遍历所有先前选择的值以使其起作用。
\ $ \ endgroup \ $
–rolfl
2014年8月28日14:46

\ $ \ begingroup \ $
这需要更好地解释。 (如果第二个数字比第一个数字大,那么第二个数字将增加1;如果两个数都大于一个,则第二个数字将增加2;如果大于一个,则第三个数字增加1,依此类推。)
\ $ \ endgroup \ $
– ANeves认为SE是邪恶的
14年8月28日在16:46

#9 楼

从它的外观看,您想要的就是所有int值的(伪)随机排列。请注意,您不必一定将这些值存储在数组中并对其进行混洗,从而消耗内存。您实际上可以通过“计数”变量中各个位的“完美”哈希函数按需生成它们。您必须确保每次使用不同的值“播种”您的序列。

#10 楼

您有两个参数:想要的整数个数N,以及它们的最大大小M> =N。如果N接近M,例如,您想要500个数字在1..1000范围内,那么最好的选择是(部分)将数字1..M打乱例如,您可以在1..M上运行Fisher-Yates随机播放的前N个步骤,并获取所需的数字。

否则,如果M比N大很多,则最好一次生成所有随机数(而不是一一检查它们是否在列表中),排序,删除重复项,根据需要添加新元素以进行替换,然后重新组合。

您可能希望生成N个以上的数字来处理预期的重复次数。预期重复项的数量约为N(N-1)/ 2M,因此您可能会在初始列表中生成N + 1.1N(N-1)/ 2M个数字。

排序和删除重复是标准的操作。

可以使用标准混洗(例如上述的Fisher-Yates)进行重新修饰,也可以将原始顺序与每个元素存储在一起,然后再添加以此排序。如果您这样做了,并且原来是过度生成的,则只需忽略最后的任何额外元素。测试以找到方法之间的界限。如果不是这样,则第二种方法将在所有情况下都适用-尽管如果不需要,则第一种方法更容易编程。

评论


\ $ \ begingroup \ $
OP仅具有一个参数N。所谓的第二个参数M实际上在代码中被硬编码为random.Next(),表示M为Integer.MAX_VALUE(2 ^ 31-1)。
\ $ \ endgroup \ $
–rolfl
2014年8月30日12:30

\ $ \ begingroup \ $
@rolfl:正确。我想明确说明对M的依赖。
\ $ \ endgroup \ $
–查尔斯
2014年8月31日在19:22

#11 楼

首先,有一个未指定的变量random,而我最好的选择是假设它是System.Random,并且Next方法返回


一个大于或等于32位有符号整数小于零且小于MaxValue。


我请作者在问题中阐明这一点。在此假设下,我们有适用于count < int.MaxValue的算法,但是对于count接近该值的算法,它会变得非常慢,但这意味着要消耗近8GB的内存(2G * 4B)。对于大于某个阈值的count,我们可以尝试使其速度更快,例如20(取决于基准测试),将异常抛出一些荒谬的计数,然后将其重写,如下所示:

原始代码),这样就足够了,但仍然需要进一步考虑谁可以维护代码。我将不再赘述,因为这取决于可以维护代码的人数和程序员的喜好(见到我不会讲代码风格)。我对此并没有那么严格,对我来说,两条空行(一个前一行,一个后一行)足以表明它在遇到时需要更多的思考,而我绝不会碰到任何有效且最佳的代码。注释可以帮助描述意图,如果出现错误(不起作用或运行缓慢),则应检查代码。通过使用do..while,下面的更多行尾和很少的注释,以下内容可能会看起来更好:

#12 楼

我会给出一个简短的答案。

由于您的随机数是32位整数,因此发生碰撞的可能性非常低。在这种情况下,do ... while循环只会偶尔循环,不会引起任何性能问题。

问题是如何检查碰撞。检查数字是否属于列表需要顺序读取列表。这很慢。您最好使用HashMap。将每个生成的数字插入HashMap中,并对照HashMap检查新数字。您可以在生成数字时构建列表,也可以稍后从HashMap构建列表。但是第二种解决方案会损害随机性,您将不得不再次洗牌。为了真正节省内存,您必须在构建列表时清空HashMap。一次清空一个HashMap元素会增加开销。 .MaxValue-N + 1,对列表进行排序,在第ith个元素中添加i,然后再次随机播放列表。它最大程度地减少了内存使用,但运行时间为O(N log N)。

List + HashMap解决方案的运行时间几乎为O(N),但至少需要两倍的内存。

#13 楼

如果存在1-4个可能的数字,并且您已经生成了1个数字,则意味着还剩下(4-1个)3个可能的数字。在3之间创建一个随机数,对于每个大于或等于该数字的生成数,将创建的数字增加1。 >
生成的数字:[1,2,3]
可能的数字:[1,3,4]

解决方案:

public static int[] GetRandomNumbers(int count, int minValue, int maxValue)
{
    int[] randomNumbers = new int[count]; 

    for (int i=0; i < count; ++i) 
    {      
        //Given (min - max) is the range, there can only be (range - i) unique number left
        int number = random.Next(minValue, maxValue - i);

        //if the number is greater than or equal to, then increase the number
        for (int j=0; j < i; ++j)
            if(number >= randomNumbers[j])
                ++number; 

        randomNumbers.Add(number);
    }

    return randomNumbers;
}


评论


\ $ \ begingroup \ $
+1,提供了看起来可行的解决方案,避免了可能的重复值检查。请注意,虽然您的解决方案会因较大的计数值而严重扩展。 \ $ O(n ^ 2)\ $时间复杂度
\ $ \ endgroup \ $
–rolfl
2014年8月30日12:38

\ $ \ begingroup \ $
这不会编译(数组没有Add方法),并且会产生重复的值。在将其编译后,在我的机器上,GetRandomNumbers(10,0,20,new Random(7))产生7、17、12、0、6、13、1、18、15、13。
\ $ \ endgroup \ $
–mjolka
2014年9月1日下午6:45



#14 楼

而不是使用do-while,我们不能使用Linq用更少的代码行以替代方式执行此操作吗?列表。

您需要使用System.Linq命名空间进行添加。默认情况下应该存在。

static void PrintRandom(int limit)
{

        List<int> original = Enumerable.Range(1, limit).ToList();
        Random rand = new Random ();

        //shuffle the list

        List <int> sorted = original.OrderBy(item => rand.Next()).ToList ();
        foreach (int i in sorted)
        {
            Console.WriteLine(i);   
        }
        Console.ReadKey();
}


#15 楼

理想的做法是在定义的范围内创建细分,并从细分中随机选择一个值,同时也随机抽取细分。

    public static IEnumerable<int> GetRandomNumbers(int count, int minValue, int maxValue)
    {
        if (count < 1)
        {
            throw new ArgumentException("Count must be greater then 0", "count");
        }

        if (count > Math.Abs(maxValue - minValue))
        {
            throw new ArgumentException("MaxValue must be greater than count", "maxValue");
        }

        var segmentSize = (maxValue - minValue) / count;
        var random = new Random();
        var steps = Enumerable.Range(0, count).ToList();

        while (steps.Count != 0)
        {
            var currentIndex = random.Next(0, steps.Count);
            var index = steps[currentIndex];
            steps.RemoveAt(currentIndex);

            yield return random.Next((index * segmentSize) + minValue, ((index + 1) * segmentSize) + minValue);
        }
    }

    public static IEnumerable<int> GetRandomNumbers(int count)
    {
        return GetRandomNumbers(count, 0, int.MaxValue);
    }


评论


\ $ \ begingroup \ $
这不起作用。 GetRandomNumbers(3,0,4)始终返回0、1、2。
\ $ \ endgroup \ $
–mjolka
2014年9月7日在8:00

\ $ \ begingroup \ $
正确,但是根据业务需求,它可以以非常简单的方式解决问题。并且可以通过随机扩展具有(maxValue-minValue)%count的段来解决该问题。
\ $ \ endgroup \ $
– Peter Kiss
2014年9月7日在8:42

\ $ \ begingroup \ $
但是,如果我想从[0,4)范围内随机选择三个数字,即GetRandomNumbers(3,0,4),我希望3出现的可能性相等,分别为0、1和2。
\ $ \ endgroup \ $
–mjolka
2014年9月7日上午9:12

#16 楼

List包含\ $ O(n)\ $。 HashSet包含的是\ $ O(1)\ $。

private static Random rand = new Random();
public static IEnumerable<int> GetRandomNumbers(int count)
{
    HashSet<int> randomNumbers = new HashSet<int>();
    while(randomNumbers.Count < count)
    {
        int r = rand.Next();
        if (randomNumbers.Add(r))
            yield return r;
    }
}