【问题标题】:How could I improve this C# randomising method?我该如何改进这种 C# 随机化方法?
【发布时间】:2010-12-10 06:27:14
【问题描述】:

我认为我已经将此作为随机化列表的最简单和可单元测试的方法,但我很想听听任何改进。

public static IList<T> RandomiseList<T>(IList<T> list, int seed)
{
    Random random = new Random(seed);
    List<T> takeFrom = new List<T>(list);
    List<T> ret = new List<T>(takeFrom.Count);

    while (takeFrom.Count > 0)
    {
        int pos = random.Next(0, takeFrom.Count - 1);
        T item = takeFrom[pos];
        takeFrom.RemoveAt(pos);
        ret.Add(item);
    }

    return ret;
}

【问题讨论】:

  • 只是出于兴趣,您将如何对结果应该是随机的方法进行单元测试?
  • @simonn:在这种情况下,如果你传入相同的种子,你将得到相同的订单。
  • @simonn:您应该将同一个列表随机化数百或数千次,并构建结果的直方图。任何项目以任何位置结尾的概率应该相等。您可以只关注结果,但您确实应该以 5% 的置信区间进行统计检验。

标签: c# sorting random


【解决方案1】:

您想要随机播放,最好的方法是 Fisher-Yates 随机播放:

public static IList<T> Randomise<T>(IList<T> list, int seed) 
{
    Random rng = new Random(seed); 

    List<T> ret = new List<T>(list);      
    int n = ret.Length;            
    while (n > 1) 
    {
        n--;                         
        int k = rng.Next(n + 1);  
        // Simple swap of variables
        T tmp = list[k];
        ret[k] = ret[n];
        ret[n] = tmp;
    }
    return ret;
}

【讨论】:

  • 哦,这就是我想要的!你打败了我! :)
  • 这不就是他所拥有的吗?
  • 不,这不是他所拥有的。他正在将项目从一个列表移动到另一个列表,而不是在原地交换。
  • 好的。当然。但它们都是 Fisher-Yates,因此导致所有输出订单的概率相等。
  • 所以基本上,从列表的末尾开始,您将每个项目与随机选择的另一个项目交换?这是有道理的——干杯!
【解决方案2】:

没有统计数据支持这一点,但如果您的返回值以与列表长度相同的数组开始,然后将值直接插入随机生成的索引中,这似乎会更好。

【讨论】:

  • 但是你必须考虑冲突,显而易见的方法(选择第一个空闲槽)给出了错误的分布
  • 根据 JS Bangs 的建议,我想我已经通过指定返回列表的初始容量来完成此操作。
  • 我实际上的意思是直接放置到索引而不是使用 add 但显然 Joel Cahoon 的建议是最好的 - 它有一个算法名称
【解决方案3】:

这对我来说看起来不错。请注意,如果您将 ret 初始化为 list 的长度,您将获得稍微更好的性能(尤其是对于大型列表),这样就不必重新分配列表:

List<T> ret = new List<T>(list.Count);

【讨论】:

  • 这没什么用。大多数时间显然会花在 RemoveAt 方法中,因为它每次都必须移动被删除项之后的所有项目。只要那个还在那里,试图在别处保存任何东西都是毫无意义的。
【解决方案4】:

不确定这有多大的改进,但如果列表很大并且您只需要前几个随机项,则会有性能优势。

public static IEnumerable<T> RandomiseList<T>(IList<T> list, int seed)
{
    Random random = new Random(seed);
    List<T> takeFrom = new List<T>(list);

    while (takeFrom.Count > 0)
    {
        int pos = random.Next(0, takeFrom.Count - 1);
        T item = takeFrom[pos];
        takeFrom.RemoveAt(pos);
        yield return item;
    }
}

不再需要临时列表甚至临时交换变量。

如果我要经常使用它,我会将它重写为扩展方法。

【讨论】:

  • 另外,如果你真的需要的话,在调用这个来取回你的列表之后,它只需要 9 个额外的字符。
  • @Guffa:你推荐什么?一个 HashSet 来跟踪已经滚动的索引?
  • @Joel:列表工作正常。您只需交换项目,以便将未使用的项目放在最后。实际上,您不必交换它们,因为退回的物品将永远不会再使用,您只需移动一件并退回另一件。请参阅我发布的实现。
【解决方案5】:

我喜欢 Dennis Palmers 的想法,即返回一个打乱的 IEnumerable,而不是在适当的位置打乱列表,但是使用 RemoveAt 方法会使它变慢。这是没有 RemoveAt 方法的替代方法:

public static IEnumerable<T> Shuffle<T>(IEnumerable<T> list, int seed) {
  Random rnd = new Random(seed);
  List<T> items = new List<T>(list);
  for (int i = 0; i < items.Count; i++) {
    int pos = rnd.Next(i, items.Count);
    yield return items[pos];
    items[pos] = items[i];
  }
}

我用 10000 个整数尝试了这个,它快了大约 30 倍。

【讨论】:

  • 非常好。可以通过倒数来改进(更简单的 rnd.Next 调用)+1
  • 很想删除我自己的来支持这个。需要先为你争取更多的选票,所以它最终排在我之后的第二位。
  • @Joel:我不认为你应该删除你的答案,它清楚地展示了有效洗牌的基本原则。
  • 两个答案都很好,但这个真的很摇滚。 +1 !
【解决方案6】:

您正在寻找什么样的建议?效率?正确性?你确实提到了单元测试......我认为那里肯定会有改进。

我实际上帮助开发了一款在线游戏及其洗牌机制。我真的不怀疑性能是一个很大的问题,因为您发现的大多数算法大体上都是相同的。但是,我会建议以下内容,

一个。创建随机界面

public interface IRandom
{
    byte NextRandomByte ();
}

现在使用此接口的任何东西现在都可以在受控方式或环境中进行模拟\单元测试。您并不是真的想对真正随机的算法进行单元测试——您将无法验证您的数据!

至于为什么要返回一个字节,一个字节可能是你想要的最小随机单位。不仅如此,如果给定一种生成单个随机字节的方法,生成它们的序列并将它们连接在一起是一种生成更广泛随机数据的简单方法。

当然,您必须警惕在数据中引入偏见......

b.通过减少任意间隔的偏差来确保数据质量。假设基础数据是均匀随机的,任何不是 256 倍的区间都会引入偏差。考虑一下,

// 250 is not a factor of 256!
byte a = random.NextRandomByte () % 250; // values 0-5 are biased!

在前面的 sn-p 中,值 0-5 出现的概率为 2/255,而值 6-249 的出现概率为 1/255。随着时间的推移,这是一个重大的偏差。一种方法是检查来自生成器的数字,如果超出可接受范围则丢弃它

// continually generate random data until it is satisfactory
for (byte r = random.NextRandomByte (); r > 250; r = random.NextRandomByte ())
{
}
byte a = r % 250; // r is guaranteed to be on [0, 250], no longer bias

“可接受范围”可以通过查找可以由您的值类型表示的区间的最大倍数来确定。更通用的形式

byte modulo; // specified as parameter
byte biasThreshold = (byte.MaxValue / modulo) * modulo;
for (; unbiasedValue >= biasThreshold; )
{
    // generate value
    unbiasedValue = random.NextRandomByte ();
}

如果您想要大于字节的值,只需将这些值连接在一起,

int modulo; // specified as parameter
int biasThreshold = (int.MaxValue / modulo) * modulo;
for (; unbiasedValue >= biasThreshold; )
{
    // generate value
    byte a = random.NextRandomByte ();
    byte b = random.NextRandomByte ();
    ... 
    int unbiasedValue = a << 24 + b << 16 + c << 8 + d;
}

c。消耗!将您的算法或助手放在无状态扩展或静态类中,例如

// forgive my syntax, recalling from memory
public static class IRandomExtensions
{
    public int GetUnbiasedInteger (this IRandom random, int modulo) { }
    public int GetUnbiasedUnsignedInteger (this IRandom random, uint modulo) { }
    public int GetUnbiasedLong (this IRandom random, long modulo) { }
    public int GetUnbiasedUnsignedLong (this IRandom random, ulong modulo) { }
    ...
}

public static class IEnumerableExtensions
{
    public IEnumerable<T> Shuffle<T>(this IEnumerable<T> items, IRandom random) 
    {
        // shuffle away!
        ...
    }

}

决定是否将这些实现为接口上的方法或外部方法 [正如我所做的那样] 取决于您 - 但请记住,使它们成为成员方法会迫使实现者重复或复制代码。就个人而言,我喜欢扩展。他们很干净。而且很性感。

int randomNumber = random.UnbiasedInteger (i - 1);
List<int> shuffledNumbers = numbers.Shuffle (random);

显然,前面的所有内容都是可选的,但有助于单元测试并提高随机数据的整体质量。

一般来说,随机和“公平”的骰子是一个非常有趣的话题。如果您有兴趣,我强烈建议您找个时间谷歌一下并进行一些研究。 :)

【讨论】:

  • 实际上,内置的 prng 已经可以单独进行单元测试了。只要继续传递相同的种子,您就会不断获得相同的值。当然,关键是隔离。最终,您想要测试依赖它并使用真实种子的代码。但即使在这里,抽象种子选择器可能更简单,这样您就可以为测试获得一致的种子。
  • 嗯,你们中的一些人可能想知道这有多大用处。一方面,正如我所说,您可以预测地进行单元测试。另一方面,在 SIT 中,我有几个 IRandom 的实现,您可能仅凭名称就可以猜出它们的底层生成器 - ByteQueueRandom、GuidRandom、CryptographicRandom。通过配置更改,我可以从可预测的变为伪随机的。哦,从语法上讲,myList.Shuffle(random) 看起来也很可爱:)
  • @Joel Coehoorn 你完全正确,我有点忽略了这一点。但是,如果在控制生成器或播种其他人的实现之间做出选择 - 但是 不太可能 将 wrt 更改为我的输入 - 我宁愿选择控制。
【解决方案7】:

注意看起来不错但经不起测试的幼稚洗牌算法的风险!

查看此excellent article 以获取示例。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-22
    • 1970-01-01
    相关资源
    最近更新 更多