NET随机化数组的最佳方法


141

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

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


1
这是一个奇怪但简单的解决方案-stackoverflow.com/a/4262134/1298685
伊恩·坎贝尔

1
使用MedallionRandom NuGet程序包,这仅仅是myArray.Shuffled().ToArray()(或者myArray.Shuffle()如果您想突变当前数组)
ChaseMedallion,2016年

Answers:


171

如果您使用的是.NET 3.5,则可以使用以下IEnumerable凉爽性(VB.NET,而不是C#,但是思路应该很清楚...):

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文档中所述,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是基于时间的,因此,如果在高度并发的系统中使用此代码,则可能有两个请求可以获取相同的值(例如,在
webapp

2
只是为了澄清上述内容,System.Random将使用当前时间进行播种,因此同时创建的两个实例将生成相同的“随机”序列
。System.Random

8
同样,该算法为O(n log n),并受Qsort算法偏置。请参阅我的答案以获得O(n)无偏解。
马特·豪威尔斯

9
除非OrderBy在内部缓存排序键,否则还存在违反有序比较的传递属性的问题。如果曾经有调试模式验证OrderBy产生了正确的结果,那么理论上它可能会引发异常。
山姆·哈威尔2009年


205

以下实现使用Fisher-Yates算法(又称为Knuth Shuffle)。它运行时间为O(n),并在适当位置随机播放,因此尽管“代码随机排序”技术更多,但其性能要优于“随机排序”技术。有关一些比较性能的测量,请参见此处。我使用了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算法没有偏见,因此混洗效果与您使用的RNG一样好。


1
更改参数并使用法类似array.Shuffle(new Random());.. 会更好吗?
肯健

从框架4.0开始,您可以使用元组简化交换->(array [n],array [k])=(array [k],array [n]);
dynamichael

@肯健:不,这会很不好。原因是new Random()使用基于当前系统时间的种子值初始化的,该种子值仅每〜16ms更新一次。
Matt Howells

在对此与列表removeAt解决方案进行的一些快速测试中,在999个元素上存在很小的差异。这种差异在99999个随机整数处急剧增加,该解决方案在3ms处出现,另一个在1810ms处出现。
galamdring

18

您正在寻找一种改组算法,对吗?

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

哑巴方式

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

该算法效果很好,但是请确保您的随机数生成器不太可能标记具有相同数字的两个字符串。由于所谓的“ 生日悖论”,因此发生这种情况的频率超出了您的预期。它的时间复杂度为O(n log n)。

聪明的方法

我将其描述为递归算法:

随机播放大小为n的数组(索引在[0 .. n -1] 范围内):

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

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

时间复杂度为O(n)。


8

该算法简单但效率不高,O(N 2)。所有的“排序依据”算法通常为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(N 2)的原因很微妙:List.RemoveAt()是O(N)操作,除非您从头开始依次删除。


2
这具有和knuth shuffle相同的效果,但是效率不高,因为它涉及减少一个列表并重新填充另一个列表。将项目交换到位将是更好的解决方案。
尼克·约翰逊

1
我发现这是一种优雅且易于理解的东西,在500个字符串上没有什么区别……
Sklivvz

4

您也可以使用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 ...您在类级别上声明了它,但将其用作本地方法...
Yaron

1

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

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

* 5是任意的。


从数组中随机读取可能会多次击中某些项而错过其他项!
Ray Hayes

随机播放算法已损坏。实际上,您必须使自己的任意5值非常高,然后您的混洗才能公正地进行。
皮塔鲁

制作(整数)索引的数组。随机排列索引。只需以随机顺序使用索引。没有重复,内存中的字符串引用也没有改组(可能会触发实习生,而不会)。
Christopher

1

只是不去想,您可以这样做:

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);
}

0

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

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


0
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);
}

0

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

不能满足这些要求会在排序例程中引起许多问题,包括无限循环的可能性。

关于将随机数值与每个条目相关联然后按该值排序的解决方案,这会导致输出中的固有偏差,因为每当两个条目被分配相同的数值时,输出的随机性就会受到损害。(在“稳定”排序例程中,以输入中的第一者为准,以输出中的第一者为准。Array.Sort并非稳定,但基于Quicksort算法进行的分区仍然存在偏差。)

您需要考虑所需的随机性级别。如果您在一个扑克网站上需要加密的随机性级别,以保护自己免受确定的攻击者的侵害,则您的要求与只想将歌曲播放列表随机化的用户有很大不同。

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


0

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

不久前,我写了几篇文章,介绍了使用这种技术来实现全部和部分混洗,(第二个链接是我希望增加价值的地方),以及关于如何检查实现是否公正的后续文章,可以用来检查任何随机播放算法。您可以在第二篇文章的末尾看到一个简单错误对随机数选择的影响。


1
您的链接仍然断开:/
Wai Ha Lee

0

好的,这显然是对我不利的一件事(对不起……),但是我经常使用一种相当通用且加密强度高的方法。

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()

该方法在排序时也不会让人感到惊讶,因为排序值是生成的,并且每个序列中的每个元素仅记住一次。


0

您不需要复杂的算法。

简单的一行:

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

请注意,我们需要将转换ArrayList第一,如果你不使用List摆在首位。

另外,请注意,这对于非常大的阵列而言效率不高!否则,它干净简单。


错误:运算符“。” 不能应用于类型为“ void”的操作数
有用的2016年

0

这是一个完整的工作控制台解决方案,基于此处提供的示例

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;
    }         
}

0
        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();

-1

这是使用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);

-2
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 => generator.Next()).ToArray();
T_D
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.