使用Random和OrderBy是一种很好的随机播放算法吗?


164

我在Coding Horror上阅读了一篇有关各种随机播放算法的文章。我已经看到有人在这样做的地方随机排列列表:

var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());

这是一个很好的随机播放算法吗?究竟如何运作?这是可以接受的方式吗?

Answers:


205

这不是我喜欢的一种改组方式,主要是因为它很容易实现O(n)改组,而没有充分的理由是O(n log n)。问题“工作”中的代码基本上是为每个元素赋予一个随机(希望是唯一的!)数字,然后根据该数字对元素进行排序。

我更喜欢Durstenfield的Fisher-Yates shuffle的变体,它可以交换元素。

实现一个简单的Shuffle扩展方法基本上将包括调用ToListToArray在输入上,然后使用现有的Fisher-Yates实现。(Random将参数作为参数传递,以使生活更美好。)周围有很多实现方式……我可能在某个地方给出了答案。

这种扩展方法的好处是,读者可以清楚地知道您实际上在尝试做什么。

编辑:这是一个简单的实现(没有错误检查!):

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    // Note i > 0 to avoid final pointless iteration
    for (int i = elements.Length-1; i > 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        int swapIndex = rng.Next(i + 1);
        T tmp = elements[i];
        elements[i] = elements[swapIndex];
        elements[swapIndex] = tmp;
    }
    // Lazily yield (avoiding aliasing issues etc)
    foreach (T element in elements)
    {
        yield return element;
    }
}

编辑:下面对性能的评论提醒我,我们在洗牌时实际上可以返回元素:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        // ... except we don't really need to swap it fully, as we can
        // return it immediately, and afterwards it's irrelevant.
        int swapIndex = rng.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}

现在,这只会完成所需的工作。

请注意,在两种情况下,您都需要注意Random用作以下用途的实例:

  • Random大致同时创建两个的实例将产生相同的随机数序列(以相同方式使用时)
  • Random 不是线程安全的。

我有一篇文章,Random其中将详细介绍这些问题并提供解决方案。


5
好吧,我想说的这样小的但重要的实现总是可以在StackOverflow上找到。是的,请,如果您想=)
Svish

9
乔恩(Jon)-您对Fisher-Yates的解释等同于问题中给出的实现(朴素的版本)。Durstenfeld / Knuth不是通过分配而是通过从递减集合中进行选择和交换来实现O(n)。这样,所选的随机数可以重复,并且算法仅占用O(n)。
tvanfosson

8
您可能已经厌烦了我的来信,但是我在单元测试中遇到了一个可能要注意的小问题。ElementAt有一个怪癖,使它每次都调用扩展,结果不可靠。在我的测试中,我在检查结果之前将结果具体化,以避免出现这种情况。
tvanfosson

3
@tvanfosson:一点都没病:)但是,是的,调用者应该意识到它是惰性的。
乔恩·斯基特

4
有点晚了,但是请注意,source.ToArray();要求您具有using System.Linq;相同的文件。如果不这样做,则会出现以下错误:'System.Collections.Generic.IEnumerable<T>' does not contain a definition for 'ToArray' and no extension method 'ToArray' accepting a first argument of type 'System.Collections.Generic.IEnumerable<T>' could be found (are you missing a using directive or an assembly reference?)
Powerlord

70

这是基于Jon Skeet的答案

在该答案中,将对数组进行混洗,然后使用返回yield。最终结果是,数组在foreach的持续时间内以及迭代所需的对象都保留在内存中,而代价却只是开始-收益率基本上是一个空循环。

该算法在游戏中经常使用,在游戏中,前三个项目被选中,而其他项目则仅在以后才需要。我的建议是yield尽快交换数字。这将减少启动成本,同时将迭代成本保持在O(1)(每次迭代基本5次操作)。总成本将保持不变,但改组本身会更快。在collection.Shuffle().ToArray()理论上这没有什么区别,但是在上述用例中,它将加快启动速度。而且,这将使该算法在您只需要几个唯一项的情况下很有用。例如,如果您需要从52张套牌中抽出三张牌,您可以跟注,deck.Shuffle().Take(3)并且只会进行三次交换(尽管必须先复制整个阵列)。

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    // Note i > 0 to avoid final pointless iteration
    for (int i = elements.Length - 1; i > 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        int swapIndex = rng.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
        // we don't actually perform the swap, we can forget about the
        // swapped element because we already returned it.
    }

    // there is one item remaining that was not returned - we return it now
    yield return elements[0]; 
}

哎哟! 这可能不会返回源中的所有项目。您不能依赖于N次迭代唯一的随机数。
P爸爸2009年

2
聪明!(而且我讨厌这15个字符的东西...)
Svish

@P爸爸:嗯?关心详细吗?
Svish

1
或者,您也可以将> 0替换为> = 0,而不必这样做(尽管额外的RNG命中加上多余的任务)
FryGuy

4
启动成本是O(N)作为source.ToArray();的成本。
Dave Hillier

8

从Skeet的这句话开始:

这不是我喜欢的一种改组方式,主要是因为它很容易实现O(n)改组,而没有充分的理由是O(n log n)。问题“工作”中的代码基本上是给每个元素一个随机的(希望是唯一的!)数字,然后根据该数字对元素进行排序。

我将继续解释产生独特希望的原因

现在,从Enumerable.OrderBy中

此方法执行稳定的排序;也就是说,如果两个元素的键相等,则保留元素的顺序

这个非常重要!如果两个元素“接收”相同的随机数会怎样?碰巧它们保持与数组中相同的顺序。现在,发生这种情况的可能性是什么?确切地计算很困难,但是生日问题恰恰就是这个问题。

现在,这是真的吗?是真的吗

与往常一样,如有疑问,请编写一些程序行:http : //pastebin.com/5CDnUxPG

这小段代码使用向后完成的Fisher-Yates算法,向前完成的Fisher-Yates算法(在Wiki页面中有两种伪代码算法)将一定数量的3个元素的数组混洗一定次数。结果,但一个是从第一个元素到最后一个元素完成的,而另一个是从最后一个元素到第一个元素完成的),http://blog.codinghorror.com/the-danger-of-naivete/的天真错误算法,并使用.OrderBy(x => r.Next()).OrderBy(x => r.Next(someValue))

现在是Random.Next

一个大于或等于0且小于MaxValue的32位有符号整数。

所以相当于

OrderBy(x => r.Next(int.MaxValue))

要测试是否存在此问题,我们可以扩大数组(速度很慢),或者简单地减小随机数生成器的最大值(int.MaxValue这不是“特殊”数字……这只是一个非常大的数字)。最后,如果算法不受的稳定性影响OrderBy,则任何范围的值都应给出相同的结果。

然后,程序将测试一些值,范围为1 ... 4096。从结果看,很明显,对于低值(<128),该算法有很大偏差(4-8%)。使用3个值,您至少需要r.Next(1024)。如果将数组变大(4或5),那么甚至r.Next(1024)还不够。我不是混洗和数学专家,但我认为对于数组长度的每一额外位,您都需要2个额外的最大值(因为生日悖论已连接到sqrt(numvalues)),因此如果最大值是2 ^ 31,我会说您应该能够对最多2 ^ 12/2 ^ 13位(4096-8192个元素)的数组进行排序


说得很好,并且完美地显示了原始问题的问题。这应该与乔恩的答案合并。
TheSoftwareJedi

6

对于大多数目的来说,这可能是可行的,并且几乎总是会生成真正的随机分布(除非Random.Next()生成两个相同的随机整数)。

它通过为序列的每个元素分配一个随机整数,然后按这些整数对序列进行排序来工作。

完全可以接受99.9%的应用程序(除非您绝对需要处理上述边缘情况)。而且,skeet对其运行时的反对是有效的,因此,如果您拖曳一长串列表,则可能不希望使用它。


4

这已经出现过很多次了。在StackOverflow上搜索Fisher-Yates。

这是我为此算法编写的C#代码示例。如果愿意,可以将其参数化为其他类型。

static public class FisherYates
{
        //      Based on Java code from wikipedia:
        //      http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
        static public void Shuffle(int[] deck)
        {
                Random r = new Random();
                for (int n = deck.Length - 1; n > 0; --n)
                {
                        int k = r.Next(n+1);
                        int temp = deck[n];
                        deck[n] = deck[k];
                        deck[k] = temp;
                }
        }
}

2
您不应该Random像这样使用静态变量- Random线程安全。参见csharpindepth.com/Articles/Chapter12/Random.aspx
Jon Skeet

@乔恩·斯基特(Jon Skeet):当然,这是一个合理的论点。OTOH,OP正在询问完全错误的算法,而这是正确的(除了多线程卡改组用例之外)。
hughdbrown

1
这仅意味着这比OP的方法“没有错”。这并不意味着应该在不理解无法在多线程上下文中安全使用它的情况下使用代码……这是您没有提到的。有一个合理的期望,就是可以从多个线程安全地使用静态成员。
乔恩·斯基特

@乔恩·斯基特:当然,我可以更改它。做完了 我倾向于认为回到三年半前回答的问题,并说:“这是不正确的,因为它不能处理多线程用例”,而OP从来没有问过太多的算法。回顾我多年来的答案。通常,我给OP的答复超出了规定的要求。我一直为此受到批评。不过,我不希望操作人员获得适合所有可能用途的答案。
hughdbrown 2013年

我只是访问了此答案,因为其他人在聊天中向我指出了该答案。尽管OP没有特别提到线程,但是当静态方法不是线程安全的时,我认为绝对值得一提,因为它很不寻常,并且使代码不适合许多情况而无需修改。您的新代码是线程安全的-但仍然不是理想的,就像您在“大约”同时从多个线程调用它来随机播放相同大小的两个集合一样,随机播放是等效的。基本上,Random使用起来很麻烦,如我的文章所述。
乔恩·斯基特

3

如果您不太担心性能的话,这似乎是一种很好的改组算法。我要指出的唯一问题是它的行为是不可控制的,因此您可能很难测试它。

一种可能的选择是将种子作为参数传递给随机数生成器(或将随机生成器作为参数),因此您可以进行更多控制并更轻松地对其进行测试。


3

我发现Jon Skeet的回答是完全令人满意的,但是我客户的自动扫描程序会报告任何有关Random安全漏洞的情况。所以我换了System.Security.Cryptography.RNGCryptoServiceProvider。另外,它修复了提到的线程安全问题。另一方面,RNGCryptoServiceProvider被测得比使用慢300倍Random

用法:

using (var rng = new RNGCryptoServiceProvider())
{
    var data = new byte[4];
    yourCollection = yourCollection.Shuffle(rng, data);
}

方法:

/// <summary>
/// Shuffles the elements of a sequence randomly.
/// </summary>
/// <param name="source">A sequence of values to shuffle.</param>
/// <param name="rng">An instance of a random number generator.</param>
/// <param name="data">A placeholder to generate random bytes into.</param>
/// <returns>A sequence whose elements are shuffled randomly.</returns>
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, RNGCryptoServiceProvider rng, byte[] data)
{
    var elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        rng.GetBytes(data);
        var swapIndex = BitConverter.ToUInt32(data, 0) % (i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}

3

寻找算法?您可以使用我的ShuffleList课程:

class ShuffleList<T> : List<T>
{
    public void Shuffle()
    {
        Random random = new Random();
        for (int count = Count; count > 0; count--)
        {
            int i = random.Next(count);
            Add(this[i]);
            RemoveAt(i);
        }
    }
}

然后,像这样使用它:

ShuffleList<int> list = new ShuffleList<int>();
// Add elements to your list.
list.Shuffle();

它是如何工作的?

让我们以5个第一整数的初始排序列表:{ 0, 1, 2, 3, 4 }

该方法首先计算元素的nubmer并调用它count。然后,随着count步数的减少,它在0和之间取一个随机数count,并将其移到列表的末尾。

在下面的分步示例中,可以移动的项目为斜体,所选项目为粗体

0 1 2 3 4
0 1 2 3 4
0 1 2 4 3
0 1 2 4 3
1 2 4 3 0
1 2 4 3 0
1 2 3 0 4
1 2 3 0 4
2 3 0 4 1
2 3 0 4 1
3 0 4 1 2


那不是O(n)。单独的RemoveAt是O(n)。
狗仔队

嗯,看来你是对的,我不好!我将删除该部分。
SteeveDroz

1

该算法通过为列表中的每个值生成一个新的随机值,然后按这些随机值对列表进行排序来进行随机排序。可以将其视为在内存表中添加新列,然后用GUID填充它,然后按该列进行排序。对我来说似乎是一种有效的方式(尤其是使用lambda糖!)


1

稍微无关,但这是一个真正随机生成骰子掷骰的有趣方法(尽管它确实是多余的,但实际上已经实现了)!

Dice-O-Matic

我在此处发布此消息的原因是,他提出了一些有趣的观点,说明了他的用户对使用算法在实际骰子上改组的想法有何反应。当然,在现实世界中,这种解决方案仅适用于频谱的极端极端情况,在这些极端情况下,随机性会产生巨大影响,也许这种影响会影响金钱;)。


1

我会说很多答案,例如“此算法会通过为列表中的每个值生成一个新的随机值,然后按这些随机值对列表进行排序”来洗牌。

我认为这不会为源集合的每个元素分配随机值。取而代之的是,可能有一种运行类似Quicksort的排序算法,该算法将调用比较函数大约n log n次。某种算法确实希望此比较功能稳定并始终返回相同的结果!

IEnumerableSorter可能不是为每个算法步骤(例如quicksort)调用一个compare函数,并且每次都x => r.Next()为这两个参数调用该函数而不缓存它们!

在那种情况下,您可能真的弄乱了排序算法,并使它变得比建立该算法的期望值差得多。当然,它最终将变得稳定并返回一些东西。

稍后我可能会通过将调试输出放入新的“ Next”函数中进行检查,以便了解会发生什么。在Reflector中,我无法立即发现它是如何工作的。


1
事实并非如此:内部重写void ComputeKeys(TElement [] elements,int count); 声明类型:System.Linq.EnumerableSorter <TElement,TKEY的>装配:System.Core程序,版本= 3.5.0.0这个函数创建与所有键的阵列第一消耗存储器,快速排序它们排序前
基督教

知道这很高兴-虽然仍然只是实现细节,但可以想象在将来的版本中会有所变化!
Blorgbeard在2012年

-5

在清除所有线程并缓存每个新测试的代码上运行的启动时间,

第一个失败的代码。它在LINQPad上运行。如果您按照测试此代码。

Stopwatch st = new Stopwatch();
st.Start();
var r = new Random();
List<string[]> list = new List<string[]>();
list.Add(new String[] {"1","X"});
list.Add(new String[] {"2","A"});
list.Add(new String[] {"3","B"});
list.Add(new String[] {"4","C"});
list.Add(new String[] {"5","D"});
list.Add(new String[] {"6","E"});

//list.OrderBy (l => r.Next()).Dump();
list.OrderBy (l => Guid.NewGuid()).Dump();
st.Stop();
Console.WriteLine(st.Elapsed.TotalMilliseconds);

list.OrderBy(x => r.Next())使用38.6528毫秒

list.OrderBy(x => Guid.NewGuid())使用36.7634毫秒(从MSDN建议。)

在第二次之后,它们都同时使用。

编辑: 在Intel Core i7 4 @ 2.1GHz,Ram 8 GB DDR3 @ 1600,HDD SATA 5200 rpm和[数据:www.dropbox.com/s/pbtmh5s9lw285kp/data]上的测试代码

using System;
using System.Runtime;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Threading;

namespace Algorithm
{
    class Program
    {
        public static void Main(string[] args)
        {
            try {
                int i = 0;
                int limit = 10;
                var result = GetTestRandomSort(limit);
                foreach (var element in result) {
                    Console.WriteLine();
                    Console.WriteLine("time {0}: {1} ms", ++i, element);
                }
            } catch (Exception e) {
                Console.WriteLine(e.Message);
            } finally {
                Console.Write("Press any key to continue . . . ");
                Console.ReadKey(true);
            }
        }

        public static IEnumerable<double> GetTestRandomSort(int limit)
        {
            for (int i = 0; i < 5; i++) {
                string path = null, temp = null;
                Stopwatch st = null;
                StreamReader sr = null;
                int? count = null;
                List<string> list = null;
                Random r = null;

                GC.Collect();
                GC.WaitForPendingFinalizers();
                Thread.Sleep(5000);

                st = Stopwatch.StartNew();
                #region Import Input Data
                path = Environment.CurrentDirectory + "\\data";
                list = new List<string>();
                sr = new StreamReader(path);
                count = 0;
                while (count < limit && (temp = sr.ReadLine()) != null) {
//                  Console.WriteLine(temp);
                    list.Add(temp);
                    count++;
                }
                sr.Close();
                #endregion

//              Console.WriteLine("--------------Random--------------");
//              #region Sort by Random with OrderBy(random.Next())
//              r = new Random();
//              list = list.OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with OrderBy(Guid)
//              list = list.OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with Parallel and OrderBy(random.Next())
//              r = new Random();
//              list = list.AsParallel().OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with Parallel OrderBy(Guid)
//              list = list.AsParallel().OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with User-Defined Shuffle Method
//              r = new Random();
//              list = list.Shuffle(r).ToList();
//              #endregion

//              #region Sort by Random with Parallel User-Defined Shuffle Method
//              r = new Random();
//              list = list.AsParallel().Shuffle(r).ToList();
//              #endregion

                // Result
//              
                st.Stop();
                yield return st.Elapsed.TotalMilliseconds;
                foreach (var element in list) {
                Console.WriteLine(element);
            }
            }

        }
    }
}

结果描述:https : //www.dropbox.com/s/9dw9wl259dfs04g/ResultDescription.PNG
结果统计:https : //www.dropbox.com/s/ewq5ybtsvesme4d/ResultStat.PNG

结论:
假定:LINQ OrderBy(r.Next())和OrderBy(Guid.NewGuid())不比第一个解决方案中的用户定义的随机播放方法差。

答:它们是矛盾的。


1
第二个选项不正确,因此它的性能是无关紧要的。这仍然不能回答随机数排序是否可接受,有效或如何工作的问题。第一种解决方案也有正确性的问题,但他们不是作为大不了的。
Servy

抱歉,我想知道哪种更好的Linq OrderBy Quicksort参数?我需要测试性能。但是,我认为int类型的速度仅比Guid字符串要快,但事实并非如此。我了解MSDN为什么推荐。第一个解决方案的编辑性能与带有Random实例的OrderBy相同。
GMzo 2014年

衡量不能解决问题的代码性能的重点是什么?性能只是在两个都可行的解决方案之间做出考虑。当您有了可行的解决方案时,可以开始比较它们。
2014年

我必须有时间测试更多数据,然后完成,我保证会再次发布。假设:我认为Linq OrderBy并不比第一个解决方案差。意见:易于使用和理解。
GMzo 2014年

与非常简单的简单混洗算法相比,它的效率明显较低,但是再次,性能无关紧要。除了性能较低之外,它们还没有可靠地对数据进行改组。
2014年
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.