記事 を読んだことがあります コーディングホラー でさまざまなシャッフルアルゴリズムについて。私はどこかで人々がリストをシャッフルするためにこれをしたことを見ました:
var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());
これは良いシャッフルアルゴリズムですか?正確にどのように機能しますか?これを行うには許容できる方法ですか?
私が好きなシャッフルの方法ではありませんが、ほとんどの場合、O(n)シャッフルを実装するのが簡単なのに正当な理由がないのでO(n log n)であるという理由で。質問は、基本的に各要素にランダムな(できれば一意!)番号を与え、その番号に従って要素を並べることによって「機能します」。
私は、要素を交換する Fisher-Yates shuffle のDurstenfieldのバリアントを好みます。
単純な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
の2つのインスタンスをほぼ同時に作成すると、同じ乱数列が生成されます(同じ方法で使用した場合)Random
はスレッドセーフではありません。私は Random
に関する記事 を持っています。これはこれらの問題についてより詳細に説明し、解決策を提供します。
これは、Jon Skeetの answer に基づいています。
その答えでは、配列はシャッフルされ、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)であるという理由で。質問は、基本的に各要素にランダムな(うまくいけば一意!)番号を与え、その番号に従って要素を並べることで「機能」します。
うまくいけばユニークな理由を少し説明します!
次に、 Enumerable.OrderBy から:
このメソッドは安定したソートを実行します。つまり、2つの要素のキーが等しい場合、要素の順序は保持されます
これはとても重要です! 2つの要素が同じ乱数を「受信」するとどうなりますか?配列内の順序と同じ順序のままになることがあります。さて、これが起こる可能性は何ですか?正確に計算することは困難ですが、 誕生日の問題 があり、まさにこの問題です。
さて、それは本当ですか?本当ですか?
いつものように、疑わしいときはプログラムのいくつかの行を書いてください: http://Pastebin.com/5CDnUxPG
この小さなコードブロックは、後方で行われるFisher-Yatesアルゴリズム、前方で行われるFisher-Yatesアルゴリズムを使用して、3つの要素の配列を特定の回数シャッフルします( wiki ページには2つの擬似コードがありますアルゴリズム...それらは同等の結果を生成しますが、1つは最初から最後の要素まで行われ、もう1つは最後から最初の要素まで行われます) 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要素)まで配列をソートできるはずです。
ほとんどの目的にはほぼ問題なく、ほぼ常に真のランダム分布を生成します(Random.Next()が2つの同一のランダム整数を生成する場合を除く)。
系列の各要素にランダムな整数を割り当ててから、これらの整数でシーケンスを並べることで機能します。
アプリケーションの99.9%で完全に許容できます(上記のEdgeのケースを絶対に処理する必要がない限り)。また、skeetのランタイムに対する異議は有効であるため、長いリストをシャッフルしている場合は使用しない方がよいかもしれません。
パフォーマンスをあまり気にしないのであれば、良いシャッフルアルゴリズムのようです。私が指摘したい唯一の問題は、その動作を制御できないことです。そのため、テストに苦労するかもしれません。
可能なオプションの1つは、シードをパラメーターとして乱数ジェネレーター(またはパラメーターとしてランダムジェネレーター)に渡すことです。これにより、より多くの制御を行い、より簡単にテストできます。
これは以前にも何度も登場しています。 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;
}
}
}
Jon Skeetの答えは完全に満足できるものでしたが、私のクライアントのロボスキャナーはRandom
のインスタンスをセキュリティ上の欠陥として報告します。だから私はSystem.Security.Cryptography.RNGCryptoServiceProvider
。ボーナスとして、言及されたスレッドセーフの問題を修正します。一方、RNGCryptoServiceProvider
はRandom
を使用するよりも300倍遅いと測定されています。
使用法:
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];
}
}
「このアルゴリズムは、リスト内の各値に対して新しいランダムな値を生成し、それらのランダムな値でリストを並べ替えることによりシャッフルします」のような多くの答えは非常に間違っているかもしれません!
これは、ソースコレクションの各要素にランダムな値を割り当てるものではないと思います。代わりに、比較関数を約n log n回呼び出すQuicksortのようなソートアルゴリズムが存在する場合があります。ある種のアルゴリズムは、この比較関数が安定していることを本当に期待し、常に同じ結果を返します!
IEnumerableSorterが、たとえば、アルゴリズムの各ステップに対して比較関数を呼び出すことはできませんでした。クイックソートとその都度、これらのパラメーターをキャッシュせずに両方のパラメーターに対して関数x => r.Next()
を呼び出します!
その場合、ソートアルゴリズムを実際に台無しにして、アルゴリズムが構築される期待よりもはるかに悪くなる可能性があります。もちろん、最終的には安定し、何かを返します。
デバッグ出力を新しい「Next」関数内に入れることで後で確認し、何が起こるかを確認します。 Reflectorでは、どのように動作するのかすぐにはわかりませんでした。
少し関係ありませんが、ここでは、ダイスロールを本当にランダムに生成するための興味深い方法(実際には過剰になりますが、実際に実装されています)があります!
私がこれをここに投稿する理由は、彼が実際のサイコロでアルゴリズムを使用してシャッフルするというアイデアにユーザーがどのように反応したかについて興味深い点を示しているからです。もちろん、現実の世界では、このような解決策は、ランダム性が非常に大きな影響を及ぼし、おそらくその影響がお金に影響を与える、スペクトルの本当に極端な端にのみ対応しています;)。
アルゴリズムをお探しですか?私の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
を使用して、リストの最後に移動します。
次のステップバイステップの例では、移動できるアイテムはitalicで、選択されたアイテムはboldです。
1234
124
1243
1243
1243 0
124 = 3 0
123 0 4
123 0 4
23 0 4 1
2 3 0 4 1
3 0 4 1 2
このアルゴリズムは、リスト内の各値に対して新しいランダム値を生成し、それらのランダム値でリストを並べ替えることによりシャッフルします。メモリ内のテーブルに新しい列を追加し、GUIDを入力して、その列で並べ替えると考えてください。私にとっては効率的な方法のように見えます(特にラムダシュガーの場合!)