【问题标题】:Are we allowed to OrderBy(Random)?我们是否允许 OrderBy(Random)?
【发布时间】:2015-12-25 13:04:17
【问题描述】:

我有时使用OrderBy (item => R.NextDouble()) 对列表或数组进行洗牌,其中Random R 在其他地方初始化。

现在这显然是一种 hack,尽管在本网站的所有地方都推荐了这种方法,并且在实践中效果非常好。

但是,也有人指出,这是假设排序算法不会因更改单个项目的值而混淆,例如进入无限循环。

我的问题是,是否有某种隐含或明确的保证不会发生这种情况。我在 MSDN 中找不到任何关于它的信息。

虽然为此目的有非常好的、专门的 O(n) 算法,但我并不总是想在小项目中搜索和复制粘贴它们(我们在这里不是在谈论 prod)。他们显然是正确的解决方案。

请注意,性能根本不是问题。此外,不需要良好或安全的随机性(在这种情况下必须使用加密库) - 这只是稍微整理一下

【问题讨论】:

标签: c# .net random shuffle


【解决方案1】:

不保证根据文档工作。

我已经窥视了内部结构。排序算法首先将所有排序键捕获到一个临时数组中,然后不再调用它们。所以在当前的实现中这是安全的。一个新的 .NET 版本甚至是一个补丁都会使这项技术失效。

您可能会争辩说,出于兼容性的原因,这永远不会改变,而且这种情况的可能性是 95%(来源:我)。 Microsoft 有非常高的兼容性标准。

另一个问题是,如果您为两个键生成相同的值,它们将不会被随机定位。相反,它们将根据可能是确定性的排序算法的内部进行定位。例如,他们可能永远不会改变他们的相对顺序。

我只会在短期的一次性项目中依赖它。

本着这种精神的另一个技巧是按Guid.NewGuid() 排序。这为您节省了另一行代码,并且没有播种问题。另一方面,指南可能有一个模式,甚至是顺序的。也很成问题。

我会在生产代码的代码审查中失败。

【讨论】:

  • 感谢您指出临时数组。我猜供应商将来不会删除它,因为很多人都在滥用随机(对我们来说是的!),但这只是猜测。
  • OrderBy 被记录为使用稳定排序,因此对于相等的排序键,顺序始终是确定性的。
  • @LucasTrzesniewski 对,这太糟糕了。我也不明白他们为什么要让它稳定。这个属性在实践中从来没有用过,特别是因为我们有 ThenBy。 API 错误。
  • @LucasTrzesniewski 关闭,Enumerable.OrderBy 被记录为稳定,其他OrderBy 实现不是。 P-Linq 的OrderBy 肯定不稳定。
  • 没那么糟糕,稳定的快速排序在性能上并不比不稳定的快速排序差多少,而且肯定比在需要时使用ThenBy强制稳定要便宜。
【解决方案2】:

虽然它有效并且很可能不会改变,但不能保证。但是如果你希望 100% 确定,你可以通过使用显式投影来保证自己

enumerable.Select(item => new { Source = item, Order = R.NextDouble() }.OrderBy(item => item.Order).Select(item.Source)

这当然更冗长,但对于快速而肮脏的方法来说仍然很容易。

【讨论】:

  • 这也称为Schwartz sort。我发现这是一种非常优雅的方式来快速(就代码而言)打乱一个小列表。但我想知道为什么这里的每个人似乎都使用浮点数。相同位大小的随机整数即使不是更快也至少同样快,并且避免了所有的浮点问题。
  • @5gon12eder 谢谢你的信息!我不知道(像往常一样重新发明轮子:-)
【解决方案3】:

不能保证继续正常工作。

它不太可能像现在一样停止工作,因为这种更改需要更改键值的预计算,这在许多情况下会非常昂贵。因此,破坏它的变化不太可能发生。就调用选择器的频率而言,这也将是其他方面的可观察到的变化。不排除这种可观察到的变化(我已经对 .NET Core 进行了 PR,这减少了在其他 linq 方法中调用选择器的频率),但这种变化必须有非常大的好处,特别是因为它会增加,而不是减少调用次数。

它目前不能很好地工作,因为 Enumerable.OrderBy 是(与 linq 中的其他 OrderBy 方法不同,例如 Queryable 或 PLinq 中的方法)保证提供稳定的排序(相等的值总是在它们的原始订单),这会给您带来偏见。

它对于快速编写随机排序很有用,其中 shuffle 的质量只需要非常好。要获得更强大的随机播放,请使用以下内容:

public static class Shuffler
{
  public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, int seed)
  {
    return new ShuffledEnumerable<T>(source, seed);
  }

  public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
  {
    return new ShuffledEnumerable<T>(source);
  }

  private class ShuffledEnumerable<T> : IEnumerable<T>
  {
    private IEnumerable<T> _source;
    private int? _seed;

    public ShuffledEnumerable(IEnumerable<T> source)
    {
      _source = source;
    }

    public ShuffledEnumerable(IEnumerable<T> source, int seed)
      : this(source)
    {
      _seed = seed;
    }

    public IEnumerator<T> GetEnumerator()
    {
      Random rnd = _seed.HasValue ? new Random(_seed.GetValueOrDefault()) : new Random();
      T[] array = _source.ToArray();
      int count = array.Length;
      for (int i = array.Length - 1; i > 0; --i)
      {
        int j = rnd.Next(0, i + 1);
        if (i != j)
        {
          T swapped = array[i];
          array[i] = array[j];
          array[j] = swapped;
        }
      }
      return ((IEnumerable<T>)array).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
  }
}

如果您需要随机性的加密质量,您可以通过将 Random 的使用替换为合适的加密 PRNG 来进一步改进它。

【讨论】:

  • 关于预计算密钥的必要性很好。 -- 我不同意稳定性是一个问题,因为即使考虑到生日现象,NextDouble 的结果空间也太大,不会受此影响。
猜你喜欢
  • 2019-03-17
  • 2019-05-02
  • 2020-11-19
  • 1970-01-01
  • 1970-01-01
  • 2010-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多