RandomとOrderByは良いシャッフルアルゴリズムを使用していますか?


164

Coding Horrorでさまざまなシャッフルアルゴリズムについての記事を読みまし。私はどこかでリストをシャッフルするためにこれを行ったのを見ました:

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

これは良いシャッフルアルゴリズムですか?それはどのように正確に機能しますか?これを行うのに受け入れられる方法ですか?

回答:


205

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、これらの問題について詳細に入るとソリューションを提供しています。


5
さて、重要ではありますが、このような実装は、StackOverflowでいつでも確認できます。だからはい、あなたがしたいなら=)
Svish

9
ジョン-フィッシャーイェイツに関するあなたの説明は、質問で与えられた実装(ナイーブバージョン)と同等です。Durstenfeld / Knuthは、割り当てではなく、減少するセットからの選択とスワッピングによってO(n)を実現します。この方法では、選択された乱数が繰り返され、アルゴリズムはO(n)のみを使用します。
tvanfosson 2009

8
あなたはおそらくこれについて私から聞いてうんざりしているでしょうが、私はあなたが気づきたいかもしれない私のユニットテストでちょっとした問題に遭遇しました。ElementAtには、拡張機能を毎回呼び出すという癖があり、信頼できない結果をもたらします。私のテストでは、これを回避するためにチェックする前に結果を具体化しています。
tvanfosson 2009

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

これはジョン・スキートの答えに基づいています

その答えでは、配列がシャッフルされ、次を使用して返されます 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]; 
}

痛い!これは、ソース内のすべてのアイテムを返すとは限りません。N回の反復で一意である乱数に依存することはできません。
Pダディ

2
賢い!(そして私はこの15文字のものは嫌いです...)
Svish

@Pパパ:えっ?詳しく説明しますか?
スビッシュ、

1
または、> 0を> = 0で置き換える必要はありません(ただし、追加のRNGヒットと冗長割り当て)
FryGuy

4
起動コストは、source.ToArray();のコストとしてO(N)です。
デイブヒリアー

8

スキートのこの引用から始めて:

O(n)シャッフルを実装するのが簡単であるという正当な理由がなく、O(n log n)であるという理由で、それは私が好きなシャッフルの方法ではありません。質問のコードは、基本的に各要素にランダムな(うまくいけば一意です!)番号を付け、その番号に従って要素を並べ替えることで「機能します」。

うまくいけばユニークになる理由を少し説明します

Enumerable.OrderByから:

このメソッドは安定したソートを実行します。つまり、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要素)まで配列をソートできるはずです。


よく述べられており、元の質問の問題を完全に表示しています。これはジョンの答えとマージする必要があります。
TheSoftwareJedi

6

ほとんどの目的でおそらく問題なく、ほとんどの場合、真にランダムな分布を生成します(Random.Next()が2つの同一のランダムな整数を生成する場合を除く)。

系列の各要素にランダムな整数を割り当て、次にこれらの整数でシーケンスを並べることで機能します。

99.9%のアプリケーションで完全に許容されます(上記のエッジケースを絶対に処理する必要がない限り)。また、skeetのランタイムへの反対は有効であるため、長いリストをシャッフルする場合は、使用したくない場合があります。


4

これは以前にも何度も発生しています。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;
                }
        }
}

2
Randomこのような静的変数として使用しないでください- Randomスレッドセーフではありません。csharpindepth.com/Articles/Chapter12/Random.aspx
Jon Skeet

@ジョンスキート:確かに、それは正当な議論です。OTOH、OPは、これは正しいのですが(マルチスレッドのカードシャッフリングの使用例を除いて)完全に間違っているアルゴリズムについて尋ねていました。
hughdbrown、2013年

1
これは、これがOPのアプローチよりも「間違いが少ない」ことを意味します。マルチスレッドコンテキストで安全に使用できないことを理解せずに使用する必要があるコードという意味ではありません...これは、あなたが言及しなかったものです。静的メンバーを複数のスレッドから安全に使用できるという妥当な期待があります。
Jon Skeet、2013年

@ジョン・スキート:もちろん、私はそれを変更できます。できました。質問に戻って3年半前に答え、「マルチスレッドのユースケースを処理しないので不正解です」とOPがアルゴリズム以外のことについて何も質問しなかったのは過度であると思う傾向があります。長年にわたる私の回答を確認してください。多くの場合、私は指定された要件を超えたOP応答を提供しました。私はそのことで批判されてきました。しかし、OPがすべての可能な用途に適合する答えを得ることは期待していません。
hughdbrown 2013年

誰かがチャットで私にそれを指摘したので、私はこの回答をまったく訪問しませんでした。OPはスレッド化について具体的に言及していませんが、静的メソッドスレッドセーフでない場合は、言及する価値があると思います。新しいコードはスレッドセーフですが、同じサイズの2つのコレクションをシャッフルするために「ほぼ」同時に複数のスレッドから呼び出す場合と同様に、シャッフルは同等になります。基本的に、Random私の記事で述べたように、使用するのは面倒です。
Jon Skeet 2013年

3

パフォーマンスについてあまり心配していなければ、良いシャッフルアルゴリズムのようです。私が指摘する唯一の問題は、その動作が制御できないことです。そのため、テストに苦労するかもしれません。

可能なオプションの1つは、シードをパラメーターとして乱数ジェネレーター(またはパラメーターとしてランダムジェネレーター)に渡すことです。これにより、より簡単に制御してテストできます。


3

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

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 }

このメソッドは、要素の数を数えることから始まり、それを呼び出しますcount。次に、とcount各ステップに減少、それは間で乱数を取る0countリストの最後に移動します。

次のステップバイステップの例では、移動できるアイテムは斜体で、選択したアイテムは太字で示されています。です。

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)です。
パパラッツォ2017年

うーん、あなたは正しいようです、私の悪い!その部分を削除します。
SteeveDroz 2017年

1

このアルゴリズムは、リスト内の各値に対して新しいランダム値を生成し、それらのランダム値でリストを並べ替えることでシャッフルします。インメモリテーブルに新しい列を追加し、GUIDを入力して、その列で並べ替えると考えてください。私には効率的な方法のように見えます(特にラムダシュガーを使用している場合)。


1

少し関連性はありませんが、サイコロロールを真にランダムに生成するための興味深い方法があります(実際には非常に過剰ですが、実際に実装されています)。

Dice-O-Matic

私がこれをここに投稿する理由は、実際のサイコロよりもアルゴリズムを使用してシャッフルするという考えにユーザーがどのように反応したかについて、彼がいくつか興味深い点を示しているためです。もちろん、現実の世界では、そのような解決策は、ランダム性が非常に大きな影響を及ぼし、おそらくその影響が金銭に影響を与えるスペクトルの非常に極端な場合にのみ当てはまります;)


1

「このアルゴリズムは、リスト内の各値に対して新しいランダム値を生成してシャッフルし、それらのランダム値でリストを並べ替える」など、ここでの多くの答えは非常に間違っているかもしれません。

これは、ソースコレクションの各要素にランダムな値を割り当てないと思います。代わりに、Quicksortのように実行されるソートアルゴリズムがあり、比較関数を約n log n回呼び出す場合があります。ある種のアルゴリズムは、この比較関数が安定し、常に同じ結果を返すことを本当に期待しています!

IEnumerableSorterが、クイックソートなどのアルゴリズムステップごとに比較関数を呼び出し、毎回x => r.Next()これらをキャッシュせずに両方のパラメーターの関数を呼び出すことはできません。

その場合、ソートアルゴリズムを実際に混乱させ、アルゴリズムが構築されている期待よりもはるかに悪化させる可能性があります。もちろん、最終的には安定して何かを返します。

後でデバッグ出力を新しい「次へ」関数に入れて確認するので、何が起こるかを確認します。リフレクターでは、それがどのように機能するのかすぐにはわかりませんでした。


1
そうではありません:内部オーバーライドvoid ComputeKeys(TElement [] elements、int count); 宣言型:System.Linq.EnumerableSorter <TElement、TKey>アセンブリ:System.Core、Version = 3.5.0.0この関数は、メモリを消費するすべてのキーを含む配列を最初に作成してから、クイックソートでソートします
Christian

これは知っておくと便利です。ただし、実装の詳細ですが、将来のバージョンで変更される可能性があります。
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 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())は、最初のソリューションのユーザー定義のシャッフルメソッドよりも悪くありません。

回答:それらは矛盾しています。


1
2番目のオプションは正しくないため、パフォーマンスは重要ではありません。これでも、乱数による順序付けが受け入れ可能か、効率的か、それがどのように機能するかという問題にはまだ答えがありません。最初のソリューションは、正確な問題がありますが、そうではないよの契約の大。
軍人2014

申し訳ありませんが、Linq OrderByのQuicksortのより良い種類のパラメータは何ですか?パフォーマンスをテストする必要があります。ただし、int型は、Guidの文字列よりも速度が優れていると思いますが、そうではありません。MSDNが推奨する理由を理解しました。最初のソリューションで編集されたパフォーマンスは、ランダムインスタンスを使用したOrderByと同じです。
GMzo 2014

問題を解決しないコードのパフォーマンスを測定するポイントは何ですか?パフォーマンスは、両方が機能する 2つのソリューション間で行う必要がある考慮事項にすぎません。あなたが解決策を働いてきたときは、その後、次のことができ始めるそれらを比較します。
サービー2014

私はより多くのデータをテストする時間を持っている必要があります、そしてそれが終わったら、私は再び投稿することを約束します。想定:Linq OrderByは最初のソリューションよりも悪くないと思います。意見:使いやすく、理解しやすいです。
GMzo 2014

非常に単純な単純なシャッフルアルゴリズムよりも効率は著しく劣りますが、ここでもパフォーマンスは重要ではありません。パフォーマンスが低下するだけでなく、データを確実にシャッフルすることもできません。
サービー2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.