使用 .NET 随机化数组的最佳方法

Posted

技术标签:

【中文标题】使用 .NET 随机化数组的最佳方法【英文标题】:Best way to randomize an array with .NET 【发布时间】:2010-09-11 15:36:25 【问题描述】:

使用 .NET 随机化字符串数组的最佳方法是什么?我的数组包含大约 500 个字符串,我想用相同的字符串创建一个新的 Array,但顺序是随机的。

请在您的答案中包含一个 C# 示例。

【问题讨论】:

这是一个奇怪但简单的解决方案——***.com/a/4262134/1298685。 使用MedallionRandom NuGet 包,这只是myArray.Shuffled().ToArray()(或myArray.Shuffle(),如果你想改变当前数组) 重复Randomize a List<T> 【参考方案1】:

可能是:

Random random = new();

string RandomWord()

    const string CHARS = "abcdefghijklmnoprstuvwxyz";
    int n = random.Next(CHARS.Length);
    return string.Join("", CHARS.OrderBy(x => random.Next()).ToArray())[0..n];

【讨论】:

【参考方案2】:

如果您使用的是 .NET 3.5,则可以使用以下 IEnumerable 酷炫:

Random rnd=new Random();
string[] MyRandomArray = MyArray.OrderBy(x => rnd.Next()).ToArray();    

编辑:这是相应的 VB.NET 代码:

Dim rnd As New System.Random
Dim MyRandomArray = MyArray.OrderBy(Function() rnd.Next()).ToArray()

第二次编辑,回应 System.Random “不是线程安全的”和“仅适用于玩具应用程序”由于返回基于时间的序列的评论:如我的示例中使用的那样,Random() 是完美的线程-安全,除非您允许重新输入随机化数组的例程,在这种情况下,您无论如何都需要lock (MyRandomArray) 之类的东西,以免损坏您的数据,这也将保护rnd .

此外,众所周知,System.Random 作为熵的来源并不是很强。正如MSDN documentation 中所述,如果您正在做任何与安​​全相关的事情,您应该使用派生自System.Security.Cryptography.RandomNumberGenerator 的东西。例如:

using System.Security.Cryptography;

...

RNGCryptoServiceProvider rnd = new RNGCryptoServiceProvider();
string[] MyRandomArray = MyArray.OrderBy(x => GetNextInt32(rnd)).ToArray();

...

static int GetNextInt32(RNGCryptoServiceProvider rnd)
    
        byte[] randomInt = new byte[4];
        rnd.GetBytes(randomInt);
        return Convert.ToInt32(randomInt[0]);
    

【讨论】:

两个注意事项:1) System.Random 不是线程安全的(您已被警告过)和 2) System.Random 是基于时间的,因此如果您在高度并发的系统中使用此代码,两个请求有可能获得相同的值(即在 webapps 中) 只是为了澄清上述内容,System.Random 将使用当前时间为自己播种,因此同时创建的两个实例将生成相同的“随机”序列..System.Random 只能在玩具应用中使用 此算法也是 O(n log n) 并且受 Qsort 算法的影响。请参阅我的答案以获得 O(n) 无偏解决方案。 除非OrderBy在内部缓存排序键,否则这也存在违反有序比较传递性的问题。如果有一个调试模式验证OrderBy 产生了正确的结果,那么理论上它可能会抛出异常。 看这个:blogs.msdn.com/b/ericlippert/archive/2011/01/31/… 和 en.wikipedia.org/wiki/…【参考方案3】:

以下实现使用Fisher-Yates algorithm AKA Knuth Shuffle。它在 O(n) 时间内运行并就地随机播放,因此比“随机排序”技术性能更好,尽管它的代码行数更多。有关一些比较性能测量,请参阅here。我使用了 System.Random,它适用于非加密目的。*

static class RandomExtensions

    public static void Shuffle<T> (this Random rng, T[] array)
    
        int n = array.Length;
        while (n > 1) 
        
            int k = rng.Next(n--);
            T temp = array[n];
            array[n] = array[k];
            array[k] = temp;
        
    

用法:

var array = new int[] 1, 2, 3, 4;
var rng = new Random();
rng.Shuffle(array);
rng.Shuffle(array); // different order from first call to Shuffle

* 对于更长的数组,为了使(极大)数量的排列具有同样的可能性,有必要通过多次迭代运行伪随机数生成器 (PRNG) 以产生足够的熵。对于一个 500 个元素的数组,只有可能的 500 个元素的一小部分!使用 PRNG 可以获得排列组合。尽管如此,Fisher-Yates 算法是无偏的,因此 shuffle 将与您使用的 RNG 一样好。

【讨论】:

改参数,改成array.Shuffle(new Random());..这样的用法不是更好吗? 从框架 4.0 开始,您可以使用元组简化交换 -> (array[n], array[k]) = (array[k], array[n]); @Ken Kin:不,这会很糟糕。原因是new Random()是根据当前系统时间用种子值初始化的,每~16ms更新一次。 在这个与列表 removeAt 解决方案的一些快速测试中,999 个元素存在细微差别。在 99999 个随机整数处,差异变得很大,这个解决方案在 3 毫秒,另一个在 1810 毫秒。【参考方案4】:

这个算法简单但效率不高,O(N2)。所有“order by”算法通常都是 O(N log N)。它可能在数十万个元素以下没有什么区别,但对于大型列表来说会。

var stringlist = ... // add your values to stringlist

var r = new Random();

var res = new List<string>(stringlist.Count);

while (stringlist.Count >0)

   var i = r.Next(stringlist.Count);
   res.Add(stringlist[i]);
   stringlist.RemoveAt(i);

它是 O(N2) 的原因很微妙:List.RemoveAt() 是一个 O(N) 操作,除非你从最后按顺序删除。

【讨论】:

这与 knuth shuffle 具有相同的效果,但效率不高,因为它涉及删除一个列表并重新填充另一个列表。将物品交换到位将是更好的解决方案。 我觉得这很优雅,很容易理解,在 500 个字符串上它并没有什么区别......【参考方案5】:
        int[] numbers = 0,1,2,3,4,5,6,7,8,9;
        List<int> numList = new List<int>();
        numList.AddRange(numbers);

        Console.WriteLine("Original Order");
        for (int i = 0; i < numList.Count; i++)
        
            Console.Write(String.Format("0 ",numList[i]));
        

        Random random = new Random();
        Console.WriteLine("\n\nRandom Order");
        for (int i = 0; i < numList.Capacity; i++)
        
            int randomIndex = random.Next(numList.Count);
            Console.Write(String.Format("0 ", numList[randomIndex]));
            numList.RemoveAt(randomIndex);
        
        Console.ReadLine();

【讨论】:

【参考方案6】:

这是一个基于the example provided in here的完整工作控制台解决方案:

class Program

    static string[] words1 = new string[]  "brown", "jumped", "the", "fox", "quick" ;

    static void Main()
    
        var result = Shuffle(words1);
        foreach (var i in result)
        
            Console.Write(i + " ");
        
        Console.ReadKey();
    

   static string[] Shuffle(string[] wordArray) 
        Random random = new Random();
        for (int i = wordArray.Length - 1; i > 0; i--)
        
            int swapIndex = random.Next(i + 1);
            string temp = wordArray[i];
            wordArray[i] = wordArray[swapIndex];
            wordArray[swapIndex] = temp;
        
        return wordArray;
             

【讨论】:

【参考方案7】:
private ArrayList ShuffleArrayList(ArrayList source)

    ArrayList sortedList = new ArrayList();
    Random generator = new Random();

    while (source.Count > 0)
    
        int position = generator.Next(source.Count);
        sortedList.Add(source[position]);
        source.RemoveAt(position);
      
    return sortedList;

【讨论】:

对我来说,感觉就像您可以通过声明第二个数组而不是尝试对数组进行洗牌来提高效率和可读性,您最好尝试转换为列表、洗牌并返回到数组: sortedList = source.ToList().OrderBy(x =&gt; generator.Next()).ToArray();【参考方案8】:

您不需要复杂的算法。

只需简单的一行:

Random random = new Random();
array.ToList().Sort((x, y) => random.Next(-1, 1)).ToArray();

请注意,如果您一开始不使用List,我们需要先将Array 转换为List

另外,请注意,这对于非常大的数组效率不高!否则它很干净和简单。

【讨论】:

错误:运算符 '.'不能应用于“void”类型的操作数【参考方案9】:

好的,这显然是我这边的一个问题(道歉......),但我经常使用一种非常通用且密码学强大的方法。

public static class EnumerableExtensions

    static readonly RNGCryptoServiceProvider RngCryptoServiceProvider = new RNGCryptoServiceProvider();
    public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
    
        var randomIntegerBuffer = new byte[4];
        Func<int> rand = () =>
                             
                                 RngCryptoServiceProvider.GetBytes(randomIntegerBuffer);
                                 return BitConverter.ToInt32(randomIntegerBuffer, 0);
                             ;
        return from item in enumerable
               let rec = new item, rnd = rand()
               orderby rec.rnd
               select rec.item;
    

Shuffle() 是任何 IEnumerable 的扩展,因此可以使用 0 到 1000 的随机顺序获取列表中的数字

Enumerable.Range(0,1000).Shuffle().ToList()

这种方法在排序时也不会带来任何意外,因为排序值会在序列中的每个元素生成并只记住一次。

【讨论】:

【参考方案10】:

您还可以使用 Matt Howells 制作扩展方法。示例。

   namespace System
    
        public static class MSSystemExtenstions
        
            private static Random rng = new Random();
            public static void Shuffle<T>(this T[] array)
            
                rng = new Random();
                int n = array.Length;
                while (n > 1)
                
                    int k = rng.Next(n);
                    n--;
                    T temp = array[n];
                    array[n] = array[k];
                    array[k] = temp;
                
            
        
    

然后你可以像这样使用它:

        string[] names = new string[] 
                "Aaron Moline1", 
                "Aaron Moline2", 
                "Aaron Moline3", 
                "Aaron Moline4", 
                "Aaron Moline5", 
                "Aaron Moline6", 
                "Aaron Moline7", 
                "Aaron Moline8", 
                "Aaron Moline9", 
            ;
        names.Shuffle<string>();

【讨论】:

为什么每次调用该方法都重新创建 rng...您在类级别声明它但将其用作本地...【参考方案11】:

这篇文章已经得到了很好的回答 - 使用 Fisher-Yates shuffle 的 Durstenfeld 实现来获得快速且公正的结果。甚至已经发布了一些实现,尽管我注意到有些实际上是不正确的。

不久前我写了几篇关于implementing full and partial shuffles using this technique 的帖子,并且(这第二个链接是我希望增加价值的地方)还有a follow-up post about how to check whether your implementation is unbiased,它可以用来检查任何洗牌算法。您可以在第二篇文章的末尾看到随机数选择中的一个简单错误可能产生的影响。

【讨论】:

您的链接仍然断开:/​​【参考方案12】:

Jacco,您使用自定义 IComparer 的解决方案并不安全。排序例程要求比较器符合几个要求才能正常运行。其中首先是一致性。如果在同一对对象上调用比较器,则它必须始终返回相同的结果。 (比较也必须是可传递的)。

未能满足这些要求可能会导致排序例程出现许多问题,包括可能出现无限循环。

关于将随机数值与每个条目相关联然后按该值排序的解决方案,这些会导致输出中存在固有偏差,因为任何时候两个条目被分配相同的数值,输出的随机性将被妥协。 (在“稳定”排序例程中,输入中的第一个将是输出中的第一个。Array.Sort 碰巧不是稳定的,但仍然存在基于 Quicksort 算法完成的分区的偏差)。

您需要考虑一下您需要什么级别的随机性。如果您正在运行一个扑克网站,您需要加密级别的随机性来防止确定的攻击者攻击,那么您的要求与只想随机化歌曲播放列表的人有非常不同的要求。

对于歌曲列表改组,使用种子 PRNG(如 System.Random)没有问题。对于扑克网站来说,这甚至不是一个选项,你需要比任何人在 *** 上为你做的事情都更努力地思考这个问题。 (使用加密 RNG 只是开始,您需要确保您的算法不会引入偏差,您有足够的熵源,并且您不会暴露任何会损害后续随机性的内部状态)。

【讨论】:

【参考方案13】:

这是使用 OLINQ 的一种简单方法:

// Input array
List<String> lst = new List<string>();
for (int i = 0; i < 500; i += 1) lst.Add(i.ToString());

// Output array
List<String> lstRandom = new List<string>();

// Randomize
Random rnd = new Random();
lstRandom.AddRange(from s in lst orderby rnd.Next(100) select s);

【讨论】:

【参考方案14】:

您正在寻找洗牌算法,对吧?

好的,有两种方法可以做到这一点:聪明但人们总是似乎误解了它并得到了它错误的所以也许它不是那个聪明的毕竟方式,以及愚蠢但谁在乎,因为它有效的方式。

笨方法

创建第一个数组的副本,但应使用随机数标记每个字符串。 根据随机数对重复数组进行排序。

此算法运行良好,但请确保您的随机数生成器不太可能用相同的数字标记两个字符串。由于所谓的Birthday Paradox,这种情况发生的频率比您预期的要多。它的时间复杂度是O(n log n)。

聪明的方法

我将其描述为递归算法:

洗牌一个大小为 n 的数组(索引在 [0..n-1] 范围内):

如果 n = 0 什么都不做 如果 n > 0 (递归步骤) 打乱数组的前 n-1 个元素 在 [0..n-1] 范围内选择一个随机索引 x 将索引 n-1 处的元素与索引 x 处的元素交换

等效的迭代是遍历数组,在进行过程中与随机元素交换,但请注意,不能与迭代器指向的元素之后交换。这是一个非常常见的错误,会导致有偏见的洗牌。

时间复杂度为 O(n)。

【讨论】:

【参考方案15】:
Random r = new Random();
List<string> list = new List(originalArray);
List<string> randomStrings = new List();

while(list.Count > 0)

int i = r.Random(list.Count);
randomStrings.Add(list[i]);
list.RemoveAt(i);

【讨论】:

【参考方案16】:

只是想了想,你可以这样做:

public string[] Randomize(string[] input)

  List<string> inputList = input.ToList();
  string[] output = new string[input.Length];
  Random randomizer = new Random();
  int i = 0;

  while (inputList.Count > 0)
  
    int index = r.Next(inputList.Count);
    output[i++] = inputList[index];
    inputList.RemoveAt(index);
  

  return (output);

【讨论】:

【参考方案17】:

随机化数组是密集的,因为你必须在一堆字符串周围移动。为什么不只是从数组中随机读取?在最坏的情况下,您甚至可以使用 getNextString() 创建一个包装类。如果您确实需要创建一个随机数组,那么您可以执行类似的操作

for i = 0 -> i= array.length * 5
   swap two strings in random places

*5 是任意的。

【讨论】:

从数组中随机读取可能会多次命中某些项目而错过其他项目! 洗牌算法坏了。在你的洗牌没有偏见之前,你必须让你的任意 5 非常高。 创建一个(整数)索引数组。随机排列索引。只需按随机顺序使用索引。没有重复,没有在内存中对字符串引用进行改组(每个都可能触发实习,什么都不会)。【参考方案18】:

生成相同长度的随机浮点数或整数数组。对该数组进行排序,并对目标数组进行相应的交换。

这产生了一个真正独立的排序。

【讨论】:

以上是关于使用 .NET 随机化数组的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 中随机化整数数组的最快方法

随机化一个整数数组[重复]

Algs4-2.3.29随机化

在采摘时随机播放数组或随机化索引?

如何在 C# 中的石头、纸、剪刀游戏中随机化数组中的变量? [复制]

试图随机化一个数组,但每次都保持相同的随机化[重复]