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());
これは良いシャッフルアルゴリズムですか?それはどのように正確に機能しますか?これを行うのに受け入れられる方法ですか?
回答:
O(n)シャッフルを実装するのが簡単であるという正当な理由もなく、O(n log n)であることを主な理由として、それは私が好きなシャッフル方法ではありません。問題のコードは、基本的に各要素にランダムな(うまくいけば一意です!)番号を付け、その番号に従って要素を並べ替えることで「機能します」。
要素を交換するフィッシャーイェイツのシャッフルのダーステンフィールドのバリアントが好きです。
単純なShuffle
拡張メソッドの実装は、基本的に、呼び出しToList
またはToArray
入力時の既存のフィッシャーイェーツの実装の使用で構成されます。(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
ほぼ同時に2つのインスタンスを作成すると、同じ乱数列が生成されます(同じ方法で使用した場合)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?)
その答えでは、配列がシャッフルされ、次を使用して返されます yield
ます。最終的な結果として、配列はforeachの期間中メモリに保持され、繰り返しに必要なオブジェクトも含まれますが、コストはすべて最初に発生します。歩留まりは基本的に空のループです。
このアルゴリズムは、最初の3つのアイテムが選択されるゲームでよく使用され、その他は必要な場合にのみ後で必要になります。私の提案はyield
、それらが交換されるとすぐに数にです。これにより、開始コストが削減され、反復コストはO(1)に維持されます(基本的に反復ごとに5つの操作)。総コストは同じままですが、シャッフル自体はより速くなります。これが呼び出された場合、collection.Shuffle().ToArray()
理論的には違いはありませんが、前述の使用例では、起動が速くなります。また、これにより、このアルゴリズムは、いくつかの一意のアイテムのみが必要な場合に役立ちます。たとえば、52枚のデッキから3枚のカードを引き出す必要がある場合、呼び出すことができ、deck.Shuffle().Take(3)
スワップは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];
}
スキートのこの引用から始めて:
O(n)シャッフルを実装するのが簡単であるという正当な理由がなく、O(n log n)であるという理由で、それは私が好きなシャッフルの方法ではありません。質問のコードは、基本的に各要素にランダムな(うまくいけば一意です!)番号を付け、その番号に従って要素を並べ替えることで「機能します」。
うまくいけばユニークになる理由を少し説明します!
このメソッドは安定したソートを実行します。つまり、2つの要素のキーが等しい場合、要素の順序は保持されます
これは非常に重要です!2つの要素が同じ乱数を「受信」するとどうなりますか?配列内と同じ順序のままである場合があります。今、これが起こる可能性は何ですか?正確に計算することは困難ですが、まさにこの問題である誕生日の問題があります。
さて、それは本当ですか?本当ですか?
いつものように、疑問がある場合は、プログラムのいくつかの行を書きます:http : //pastebin.com/5CDnUxPG
この小さなコードブロックは、後方に行われるフィッシャーイェーツアルゴリズム、前方に行われるフィッシャーイェーツアルゴリズムを使用して、3つの要素の配列を特定の回数シャッフルします(wikiページには2つの疑似コードアルゴリズムがあります...その結果、他の最後から最初の要素に行われ、一方の)は、最初から最後の要素まで行われ、ナイーブの間違ったアルゴリズム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でフィッシャーイエーツを検索します。
これは、このアルゴリズム用に私が作成した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の回答は完全に満足できるものであることがわかりましたが、私のクライアントのrobo-scannerはのインスタンスを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 }
。
このメソッドは、要素の数を数えることから始まり、それを呼び出します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を入力して、その列で並べ替えると考えてください。私には効率的な方法のように見えます(特にラムダシュガーを使用している場合)。
少し関連性はありませんが、サイコロロールを真にランダムに生成するための興味深い方法があります(実際には非常に過剰ですが、実際に実装されています)。
私がこれをここに投稿する理由は、実際のサイコロよりもアルゴリズムを使用してシャッフルするという考えにユーザーがどのように反応したかについて、彼がいくつか興味深い点を示しているためです。もちろん、現実の世界では、そのような解決策は、ランダム性が非常に大きな影響を及ぼし、おそらくその影響が金銭に影響を与えるスペクトルの非常に極端な場合にのみ当てはまります;)
「このアルゴリズムは、リスト内の各値に対して新しいランダム値を生成してシャッフルし、それらのランダム値でリストを並べ替える」など、ここでの多くの答えは非常に間違っているかもしれません。
これは、ソースコレクションの各要素にランダムな値を割り当てないと思います。代わりに、Quicksortのように実行されるソートアルゴリズムがあり、比較関数を約n log n回呼び出す場合があります。ある種のアルゴリズムは、この比較関数が安定し、常に同じ結果を返すことを本当に期待しています!
IEnumerableSorterが、クイックソートなどのアルゴリズムステップごとに比較関数を呼び出し、毎回x => r.Next()
これらをキャッシュせずに両方のパラメーターの関数を呼び出すことはできません。
その場合、ソートアルゴリズムを実際に混乱させ、アルゴリズムが構築されている期待よりもはるかに悪化させる可能性があります。もちろん、最終的には安定して何かを返します。
後でデバッグ出力を新しい「次へ」関数に入れて確認するので、何が起こるかを確認します。リフレクターでは、それがどのように機能するのかすぐにはわかりませんでした。
すべてのスレッドをクリアしてすべての新しいテストをキャッシュするコードで実行するための起動時間、
最初に失敗したコード。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 msを使用します(MSDNから推奨されています)。
2回目以降は、両方とも同時に使用します。
編集:
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())は、最初のソリューションのユーザー定義のシャッフルメソッドよりも悪くありません。
回答:それらは矛盾しています。