我在Coding Horror上阅读了一篇有关各种随机播放算法的文章。我已经看到有人在这样做的地方随机排列列表:
var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());
这是一个很好的随机播放算法吗?究竟如何运作?这是可以接受的方式吗?
我在Coding Horror上阅读了一篇有关各种随机播放算法的文章。我已经看到有人在这样做的地方随机排列列表:
var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());
这是一个很好的随机播放算法吗?究竟如何运作?这是可以接受的方式吗?
Answers:
这不是我喜欢的一种改组方式,主要是因为它很容易实现O(n)改组,而没有充分的理由是O(n log n)。问题“工作”中的代码基本上是为每个元素赋予一个随机(希望是唯一的!)数字,然后根据该数字对元素进行排序。
我更喜欢Durstenfield的Fisher-Yates shuffle的变体,它可以交换元素。
实现一个简单的Shuffle
扩展方法基本上将包括调用ToList
或ToArray
在输入上,然后使用现有的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
其中将详细介绍这些问题并提供解决方案。
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?)
这是基于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];
}
从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个元素)的数组进行排序
这已经出现过很多次了。在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;
}
}
}
Random
使用起来很麻烦,如我的文章所述。
我发现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];
}
}
寻找算法?您可以使用我的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
该算法通过为列表中的每个值生成一个新的随机值,然后按这些随机值对列表进行排序来进行随机排序。可以将其视为在内存表中添加新列,然后用GUID填充它,然后按该列进行排序。对我来说似乎是一种有效的方式(尤其是使用lambda糖!)
稍微无关,但这是一个真正随机生成骰子掷骰的有趣方法(尽管它确实是多余的,但实际上已经实现了)!
我在此处发布此消息的原因是,他提出了一些有趣的观点,说明了他的用户对使用算法在实际骰子上改组的想法有何反应。当然,在现实世界中,这种解决方案仅适用于频谱的极端极端情况,在这些极端情况下,随机性会产生巨大影响,也许这种影响会影响金钱;)。
我会说很多答案,例如“此算法会通过为列表中的每个值生成一个新的随机值,然后按这些随机值对列表进行排序”来洗牌。
我认为这不会为源集合的每个元素分配随机值。取而代之的是,可能有一种运行类似Quicksort的排序算法,该算法将调用比较函数大约n log n次。某种算法确实希望此比较功能稳定并始终返回相同的结果!
IEnumerableSorter可能不是为每个算法步骤(例如quicksort)调用一个compare函数,并且每次都x => r.Next()
为这两个参数调用该函数而不缓存它们!
在那种情况下,您可能真的弄乱了排序算法,并使它变得比建立该算法的期望值差得多。当然,它最终将变得稳定并返回一些东西。
稍后我可能会通过将调试输出放入新的“ Next”函数中进行检查,以便了解会发生什么。在Reflector中,我无法立即发现它是如何工作的。
在清除所有线程并缓存每个新测试的代码上运行的启动时间,
第一个失败的代码。它在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())不比第一个解决方案中的用户定义的随机播放方法差。
答:它们是矛盾的。