随机化List <T>

mir*_*zus 795 c# generic-list

在C#中随机化通用列表顺序的最佳方法是什么?我在一个列表中有一组有限的75个数字,我想为其分配一个随机顺序,以便为抽奖类型的应用程序绘制它们.

gre*_*ade 1066

随机播放任何(I)List基于该扩展方法费雪耶茨洗牌:

private static Random rng = new Random();  

public static void Shuffle<T>(this IList<T> list)  
{  
    int n = list.Count;  
    while (n > 1) {  
        n--;  
        int k = rng.Next(n + 1);  
        T value = list[k];  
        list[k] = list[n];  
        list[n] = value;  
    }  
}
Run Code Online (Sandbox Code Playgroud)

用法:

List<Product> products = GetProducts();
products.Shuffle();
Run Code Online (Sandbox Code Playgroud)

上面的代码使用备受批评的System.Random方法来选择交换候选.它很快但不是随意的.如果你需要在shuffle中使用更好的随机性质,请使用System.Security.Cryptography中的随机数生成器,如下所示:

using System.Security.Cryptography;
...
public static void Shuffle<T>(this IList<T> list)
{
    RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
    int n = list.Count;
    while (n > 1)
    {
        byte[] box = new byte[1];
        do provider.GetBytes(box);
        while (!(box[0] < n * (Byte.MaxValue / n)));
        int k = (box[0] % n);
        n--;
        T value = list[k];
        list[k] = list[n];
        list[n] = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

这个博客(WayBack Machine)提供一个简单的比较.

编辑:自从几年前写这个答案以来,很多人都评论或写信给我,指出我比较中的一个很大的愚蠢缺陷.他们当然是对的.System.Random没有任何问题,如果按照预期的方式使用它.在我上面的第一个例子中,我在Shuffle方法中实例化了rng变量,如果要重复调用该方法,则会遇到麻烦.下面是一个固定的完整示例,基于今天从@weston收到的关于SO的真实有用的评论.

Program.cs中:

using System;
using System.Collections.Generic;
using System.Threading;

namespace SimpleLottery
{
  class Program
  {
    private static void Main(string[] args)
    {
      var numbers = new List<int>(Enumerable.Range(1, 75));
      numbers.Shuffle();
      Console.WriteLine("The winning numbers are: {0}", string.Join(",  ", numbers.GetRange(0, 5)));
    }
  }

  public static class ThreadSafeRandom
  {
      [ThreadStatic] private static Random Local;

      public static Random ThisThreadsRandom
      {
          get { return Local ?? (Local = new Random(unchecked(Environment.TickCount * 31 + Thread.CurrentThread.ManagedThreadId))); }
      }
  }

  static class MyExtensions
  {
    public static void Shuffle<T>(this IList<T> list)
    {
      int n = list.Count;
      while (n > 1)
      {
        n--;
        int k = ThreadSafeRandom.ThisThreadsRandom.Next(n + 1);
        T value = list[k];
        list[k] = list[n];
        list[n] = value;
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果list.Count是> Byte.MaxValue怎么办?如果n = 1000,则255/1000 = 0,因此do循环将是无限循环,因为box [0] <0始终为false. (28认同)
  • 我想指出,这种比较是有缺陷的.在循环中使用<code> new Random()</ code>是问题,而不是<code> Random </ code>的随机性[解释](http://stackoverflow.com/questions/3053807/random-number -in-一个环/ 3053811#3053811) (17认同)
  • 将一个Random实例传递给Shuffle方法而不是在里面创建它是一个好主意,就好像你快速连续多次调用Shuffle一样(例如,拖拽大量的短列表),这些列表都将在同一个方面被洗牌方式(例如,第一项始终移动到位置3). (9认同)
  • 只需制作`Random rng = new Random();```static`就可以解决比较帖中的问题.因为每个后续呼叫将从先前的呼叫中继续进行最后的随机结果. (7认同)
  • #2,目前尚不清楚带有加密生成器的版本是否有效,因为一个字节的最大范围是255,所以任何大于该值的列表都不会正确地进行混洗. (4认同)
  • 必须仔细检查PRNG中的熵位数.例如,要洗牌52张牌并且能够获得每个可能的洗牌牌,并以相同的概率进行,**你需要一个至少有226位种子的PRNG!**请不要使用当需要所有可能结果的相等概率时,这个答案中的方法可以改变事物,除非你做适当的计算机科学以确保它正常工作. (4认同)
  • 请注意,LinkedLists与ArrayLists的性能大幅降低.始终确保向其发送O(1)性能可索引数组. (2认同)
  • 你的比较文章有点遗漏了.`System.Random`没有你显示的那么糟糕.`new Random()`是[使用与时间相关的默认种子值](http://msdn.microsoft.com/en-us/library/h343ddh9.aspx)这就是为什么测试中根本没有随机化的原因.出于一个真正的原因,请参阅http://boallen.com/random-numbers.html,但这又有点依赖于操作系统,因此`System.Random`可能表现得更好,或者没有. (2认同)
  • 如果两个不同的线程同时使用具有共享RNG的版本,则RNG易于破坏其状态,因为它不是线程安全的(http://stackoverflow.com/a/11109361/155892). (2认同)

小智 308

如果我们只需要以完全随机的顺序对项目进行混洗(只是为了混合列表中的项目),我更喜欢这个简单而有效的代码,它通过guid命令项目...

var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很好的优雅解决方案.如果你想要一个除了guid之外的东西来产生随机性,那就按照别的顺序排序吧.例如:`var shuffledcards = cards.OrderBy(a => rng.Next());`https://compilr.com/grenade/sandbox/Program.cs (91认同)
  • @VitoDeTullio:你记错了.当您提供随机*比较函数*时,您会冒无限循环的风险; 需要比较函数来生成一致的*总订单*.随机*键*很好.这个建议是错误的,因为*guids不保证是随机的*,不是因为按随机键排序的技术是错误的. (72认同)
  • GUID意味着独特而非随机.其中一部分是基于机器的,另一部分是基于时间的,只有一小部分是随机的.http://blogs.msdn.com/b/oldnewthing/archive/2008/06/27/8659071.aspx (35认同)
  • @Doug:`NewGuid`只保证它为你提供一个独特的GUID.它不保证随机性.如果您将GUID用于创建*unique*值之外的其他目的,那么您做错了. (24认同)
  • 请不.这是错的."随意排序"完全不是一种洗牌:你引入了偏见,更糟糕的是,你冒险进入无限循环 (20认同)
  • 它以优雅的方式完成工作.许多人只是希望他们的名单改组,而不关心它是多么随机.他们更倾向于使用这种方法而不是任何其他更具学术性的方法.我知道我这样做.谢谢! (8认同)
  • 这是一个很好的答案,但是我想警告使用它的人:如果您的列表很大,这不是一个好主意。.OrderBy(RandomFunc)的O(n)表示法是n * log(n),而不是n。对于1-1000个条目,这没什么大不了的。对于一百万个条目,它将慢100倍;对于十亿个条目,它将慢1000倍。 (3认同)
  • 感谢这个 anwser,有时你只是想要一种简单的方法来稍微调整一下,这似乎就是这样。 (2认同)
  • 有没有保证每个元素只调用一次键函数?(我没有在文档中找到任何提及它。)如果是这样,这非常优雅。否则,此代码确实存在无限循环(或增加运行时间)和其他难以预测的不一致的风险。 (2认同)

Shi*_*hah 109

我对这个简单算法的所有笨重版本感到有些惊讶.Fisher-Yates(或Knuth shuffle)有点棘手但非常紧凑.如果你去维基百科,你会看到这个算法的版本反向循环,很多人似乎并不真正理解为什么它会反过来.关键原因是这个版本的算法假定您使用的随机数生成器r(a,b)具有以下两个属性:

  1. 它接受n作为单个输入参数.
  2. 它从0返回数ñ 包容性.

但.Net随机数生成器不满足#2属性.的b,而不是返回数目从0到n-1以下.如果您尝试反向使用for循环,则需要调用Random.Next(a,b)哪个添加一个额外的操作.

但是,.Net随机数生成器还有另一个很好的函数b,它返回a到b-1.这实际上非常适合具有正常for循环的该算法的实现.所以,不用多说,这是正确,高效和紧凑的实现:

public static void Shuffle<T>(this IList<T> list, Random rnd)
{
    for(var i=list.Count; i > 0; i--)
        list.Swap(0, rnd.Next(0, i));
}

public static void Swap<T>(this IList<T> list, int i, int j)
{
    var temp = list[i];
    list[i] = list[j];
    list[j] = temp;
}
Run Code Online (Sandbox Code Playgroud)

  • @Donuts - 没有.如果你这样做,你会在洗牌中增加偏见. (13认同)
  • 当`i = list.Count - 1`,即最后一次迭代时,`rnd.Next(i,list.Count)`会给你回复.因此,您需要`i <list.Count -1`作为循环条件.好吧,你不需要它,但它可以节省1次迭代;) (7认同)
  • OP:“Fischer-Yates 有点棘手。” 将会犯许多常见的实施错误之一。 (6认同)
  • 该代码无法按预期工作。最后一个数字始终为“0”或“list.Count-1”。 (5认同)
  • @ShitalShah 您答案中的当前代码没有给出正确的结果,因为它不是正确的 Fisher-Yates 洗牌。它以及链接中的代码应该已修复。 (3认同)
  • 这段代码已损坏。如果您使用 3 个字母的字符串列表,“A”、“B”和“C”,则使用此函数实际上永远不会出现 CBA 和 BCA,因为这一行: `list.Swap(0, rnd. Next(0, i));` 将其切换为以下内容可以修复它并使其成为一个有效的、无偏的伪随机函数: `list.Swap(i-1, rnd.Next(0, i));` (3认同)
  • 通过将Swap <T>分离为单独的方法,您似乎会为temp导致大量不必要的T分配. (2认同)
  • 我认为LINQ可能会降低混乱的性能,这也是不使用它的原因,特别是考虑到代码的相对简单性. (2认同)

小智 73

IEnumerable的扩展方法:

public static IEnumerable<T> Randomize<T>(this IEnumerable<T> source)
{
    Random rnd = new Random();
    return source.OrderBy<T, int>((item) => rnd.Next());
}
Run Code Online (Sandbox Code Playgroud)

  • @JohnBeyer:存在远远超过偏见来源的问题.Random只有40亿种可能的种子,远远小于中等大小的可能的shuffle数量.只能生成一小部分可能的混洗.这种偏见使由于意外碰撞导致的偏差相形见绌. (27认同)
  • ... - `Random.Next()`可能产生一个合理的伪随机的值分布,但它确实不保证这些值是唯一的.重复密钥的概率随_N_增长(非线性),直到_N_达到2 ^ 32 + 1时达到确定性.`OrderBy`QuickSort是_stable_排序; 因此,如果多个元素碰巧被赋予相同的伪随机索引值,那么它们在输出序列中的顺序将是_same_和输入序列中的顺序; 因此,偏差被引入"洗牌". (10认同)
  • 该算法存在两个重要问题: - "OrderBy"使用QuickSort变体按其(表面上是随机的)键对项目进行排序.QuickSort性能为_O(N log N)_; 相比之下,Fisher-Yates shuffle是_O(N)_.对于75个元素的集合,这可能不是什么大问题,但是对于更大的集合来说差异将变得明显. (8认同)
  • 请注意,这不是线程安全的,即使在线程安全列表中使用也是如此 (3认同)

Ada*_*gen 10

    public static List<T> Randomize<T>(List<T> list)
    {
        List<T> randomizedList = new List<T>();
        Random rnd = new Random();
        while (list.Count > 0)
        {
            int index = rnd.Next(0, list.Count); //pick a random item from the master list
            randomizedList.Add(list[index]); //place it at the end of the randomized list
            list.RemoveAt(index);
        }
        return randomizedList;
    }
Run Code Online (Sandbox Code Playgroud)

  • 你不应该像`var listCopy = list.ToList()`这样做,以避免弹出传入列表中的所有项目吗?我真的不明白为什么你想要将这些列表变为空. (4认同)

Jod*_*ell 8

编辑RemoveAt是我以前版本的一个弱点.该解决方案克服了这一点.

public static IEnumerable<T> Shuffle<T>(
        this IEnumerable<T> source,
        Random generator = null)
{
    if (generator == null)
    {
        generator = new Random();
    }

    var elements = source.ToArray();
    for (var i = elements.Length - 1; i >= 0; i--)
    {
        var swapIndex = generator.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}
Run Code Online (Sandbox Code Playgroud)

注意可选的Random generator,如果基本框架实现Random不是线程安全的或加密强度足以满足您的需要,您可以将您的实现注入操作.

Random在这个答案中可以找到适合线程安全的加密强实现的实现.


这是一个想法,以(希望)有效的方式扩展IList.

public static IEnumerable<T> Shuffle<T>(this IList<T> list)
{
    var choices = Enumerable.Range(0, list.Count).ToList();
    var rng = new Random();
    for(int n = choices.Count; n > 1; n--)
    {
        int k = rng.Next(n);
        yield return list[choices[k]];
        choices.RemoveAt(k);
    }

    yield return list[choices[0]];
}
Run Code Online (Sandbox Code Playgroud)


And*_*her 8

想法是获得具有项目和随机顺序的匿名对象,然后按照此顺序对项目重新排序并返回值:

var result = items.Select(x => new { value = x, order = rnd.Next() })
            .OrderBy(x => x.order).Select(x => x.value).ToList()
Run Code Online (Sandbox Code Playgroud)


Adr*_*Rus 7

可以使用 morelinq 包中的 Shuffle 扩展方法,它适用于 IEnumerables

安装包 morelinq

using MoreLinq;
...    
var randomized = list.Shuffle();
Run Code Online (Sandbox Code Playgroud)


VBo*_*off 6

只是想建议使用IComparer<T>and的变体List.Sort()

public class RandomIntComparer : IComparer<int>
{
    private readonly Random _random = new Random();
    
    public int Compare(int x, int y)
    {
        return _random.Next(-1, 2);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

list.Sort(new RandomIntComparer());
Run Code Online (Sandbox Code Playgroud)


Mag*_*ron 6

从 .NET 8 开始,您可以使用Shuffle()

//Argument is Span<T> or T[]
Random.Shared.Shuffle(mySpan);
Run Code Online (Sandbox Code Playgroud)

或者,对于密码学上的强随机性:

//Argument is Span<T>
RandomNumberGenerator.Shuffle(mySpan);
Run Code Online (Sandbox Code Playgroud)

对于列表,您必须先创建一个数组 ( myList.ToArray()) 如上所示进行混洗,然后从混洗后的数组创建一个新列表

  • 或者你可以这样做:`var span = CollectionsMarshal.AsSpan(list);`以避免转换为数组。 (2认同)

小智 5

如果您不介意使用两个Lists,那么这可能是最简单的方法,但可能不是最有效或不可预测的方法:

List<int> xList = new List<int>() { 1, 2, 3, 4, 5 };
List<int> deck = new List<int>();

foreach (int xInt in xList)
    deck.Insert(random.Next(0, deck.Count + 1), xInt);
Run Code Online (Sandbox Code Playgroud)


She*_*wzy 5

您可以使用这种简单的扩展方法来实现

public static class IEnumerableExtensions
{

    public static IEnumerable<t> Randomize<t>(this IEnumerable<t> target)
    {
        Random r = new Random();

        return target.OrderBy(x=>(r.Next()));
    }        
}
Run Code Online (Sandbox Code Playgroud)

你可以通过以下方式使用它

// use this on any collection that implements IEnumerable!
// List, Array, HashSet, Collection, etc

List<string> myList = new List<string> { "hello", "random", "world", "foo", "bar", "bat", "baz" };

foreach (string s in myList.Randomize())
{
    Console.WriteLine(s);
}
Run Code Online (Sandbox Code Playgroud)


Joh*_*ren 5

当希望不修改原始文件时,这是我首选的 shuffle 方法。它是Fisher–Yates“由内而外”算法的变体,适用于任何可枚举序列(source不需要从一开始就知道的长度)。

public static IList<T> NextList<T>(this Random r, IEnumerable<T> source)
{
  var list = new List<T>();
  foreach (var item in source)
  {
    var i = r.Next(list.Count + 1);
    if (i == list.Count)
    {
      list.Add(item);
    }
    else
    {
      var temp = list[i];
      list[i] = item;
      list.Add(temp);
    }
  }
  return list;
}
Run Code Online (Sandbox Code Playgroud)

该算法也可以通过将随机选择的索引与最后一个索引交换,直到所有索引都被精确选择一次来分配范围从0length - 1并随机耗尽索引。上面的代码完成了完全相同的事情,但没有额外的分配。这很整洁。

关于Random该类,它是一个通用数字生成器(如果我正在运行彩票,我会考虑使用不同的东西)。默认情况下,它还依赖于基于时间的种子值。该问题的一个小缓解是Random使用RNGCryptoServiceProvider或您可以RNGCryptoServiceProvider在类似于此的方法(见下文)中使用 来生成统一选择的随机双浮点值,但运行彩票几乎需要了解随机性和性质随机源。

var bytes = new byte[8];
_secureRng.GetBytes(bytes);
var v = BitConverter.ToUInt64(bytes, 0);
return (double)v / ((double)ulong.MaxValue + 1);
Run Code Online (Sandbox Code Playgroud)

生成随机双精度值(仅介于 0 和 1 之间)的要点是用于缩放到整数解。如果你需要从一个基于随机双倍的列表中选择一些东西,x那总是0 <= x && x < 1很简单的。

return list[(int)(x * list.Count)];
Run Code Online (Sandbox Code Playgroud)

享受!