.NETで配列をランダム化する最良の方法


141

.NETで文字列の配列をランダム化する最良の方法は何ですか?私の配列には約500個の文字列が含まれていArrayますが、同じ文字列でランダムな順序で新しい文字列を作成したいと思います。

回答にC#の例を含めてください。


1
これは奇妙ですが簡単な解決策です-stackoverflow.com/a/4262134/1298685
Ian Campbell

1
MedallionRandom NuGetパッケージを使用すると、これはmyArray.Shuffled().ToArray()(またはmyArray.Shuffle()、現在の配列を変更したい場合)
ChaseMedallion

回答:


171

.NET 3.5を使用している場合は、次のIEnumerableのクールさを使用できます(C#ではなくVB.NETですが、アイデアは明確でなければなりません...):

Random rnd=new Random();
string[] MyRandomArray = MyArray.OrderBy(x => rnd.Next()).ToArray();    

編集:OK、そしてこれが対応するVB.NETコードです:

Dim rnd As New System.Random
Dim MyRandomArray = MyArray.OrderBy(Function() rnd.Next()).ToArray()

2番目の編集、時間ベースのシーケンスを返すため、System.Randomは「スレッドセーフではない」および「おもちゃのアプリにのみ適している」というコメントに応じて:この例で使用されているように、Random()は完全にスレッドセーフですが、配列をランダム化するルーチンを再入力できるようにします。その場合lock (MyRandomArray)、データを破損しないようにするために、とにかくようなものが必要になりますrnd。これも保護されます。

また、エントロピーのソースとしてのSystem.Randomはそれほど強力ではないことをよく理解しておく必要があります。MSDNドキュメントに記載されているように、System.Security.Cryptography.RandomNumberGeneratorセキュリティ関連のことを行う場合は、派生したものを使用する必要があります。例えば:

using System.Security.Cryptography;

...

RNGCryptoServiceProvider rnd = new RNGCryptoServiceProvider();
string[] MyRandomArray = MyArray.OrderBy(x => GetNextInt32(rnd)).ToArray();

...

static int GetNextInt32(RNGCryptoServiceProvider rnd)
    {
        byte[] randomInt = new byte[4];
        rnd.GetBytes(randomInt);
        return Convert.ToInt32(randomInt[0]);
    }

2つの注記:1)System.Randomはスレッドセーフではありません(警告されています)および2)System.Randomは時間ベースなので、このコードを並行処理の多いシステムで使用すると、2つのリクエストで同じ値(つまり、webapps)
therealhoff

2
上記を明確にするために、System.Randomは現在の時間を使用して自身をシードするため、同時に作成された2つのインスタンスは同じ「ランダム」シーケンスを生成します
。System.Random

8
また、このアルゴリズムはO(n log n)であり、Qsortアルゴリズムによってバイアスされています。O(n)の公平な解決策については、私の回答を参照してください。
マットハウエルズ

9
OrderByソートキーを内部的にキャッシュしない限り、これには順序付き比較の推移的なプロパティに違反するという問題もあります。OrderBy正しい結果を生成したデバッグモードの検証がある場合、理論的には例外がスローされる可能性があります。
サム・ハーウェル


205

次の実装では、Fisher-Yatesアルゴリズム、別名Knuth Shuffleを使用しています。コードの行数は増えますが、O(n)時間で実行され、所定の位置でシャッフルされるため、「ランダムで並べ替え」手法よりもパフォーマンスが向上します。いくつかの比較パフォーマンス測定については、こちらをご覧ください。私はSystem.Randomを使用しましたが、これは非暗号化の目的には適しています。*

static class RandomExtensions
{
    public static void Shuffle<T> (this Random rng, T[] array)
    {
        int n = array.Length;
        while (n > 1) 
        {
            int k = rng.Next(n--);
            T temp = array[n];
            array[n] = array[k];
            array[k] = temp;
        }
    }
}

使用法:

var array = new int[] {1, 2, 3, 4};
var rng = new Random();
rng.Shuffle(array);
rng.Shuffle(array); // different order from first call to Shuffle

*より長い配列の場合、(非常に大きな)順列の確率を等しくするために、十分なエントロピーを生成するために、各スワップの多くの反復を通じて疑似乱数ジェネレーター(PRNG)を実行する必要があります。500要素の配列の場合、可能な500のごく一部のみです。順列は、PRNGを使用して取得できます。それでも、Fisher-Yatesアルゴリズムには偏りがないため、シャッフルは使用するRNGと同じくらい優れています。


1
パラメータを変更して、使用法をarray.Shuffle(new Random());..のようにした方がいいのではないでしょうか。
ケンキン

フレームワーク4.0以降、タプルを使用してスワップを簡略化できます->(array [n]、array [k])=(array [k]、array [n]);
dynamichael

@ケンキン:いいえ、これは悪いでしょう。その理由はnew Random()、現在のシステム時刻に基づいたシード値で初期化され、〜16msごとにのみ更新されるためです。
Matt Howells

これとリストのremoveAtソリューションのいくつかの簡単なテストでは、999要素で小さな違いがあります。違いは99999ランダム整数で劇的になり、このソリューションは3msで、もう1つは1810msです。
galamdring

18

あなたはシャッフルアルゴリズムを探していますよね?

わかりました、これを行うには2つの方法があります。方法、そしてそれは機能するので、ロックとしてのダムですが、気にしないでください。

ばかげた方法

  • 最初の配列の複製を作成しますが、各文字列には乱数をタグ付けする必要があります。
  • 乱数に関して重複した配列をソートします。

このアルゴリズムはうまく機能しますが、乱数ジェネレーターが2つの文字列に同じ番号のタグを付ける可能性が低いことを確認してください。いわゆるバースデーパラドックスが原因で、これは予想よりも頻繁に発生します。その時間の複雑さはO(n log n)です。

賢い方法

これを再帰的アルゴリズムとして説明します。

サイズnの配列をシャッフルするには(範囲[0 .. n -1]のインデックス):

n = 0の 場合
  • 何もしない
n > 0の 場合
  • (再帰的ステップ)配列の最初のn -1要素をシャッフルします
  • 範囲[0 .. n -1]のランダムなインデックスxを選択します
  • インデックスn -1の要素をインデックスxの要素と交換する

同等の反復処理とは、反復子を配列内でウォークしながらランダムな要素と入れ替えることですが、反復子が指す要素のに要素を入れ替えることはできません。これは非常に一般的な間違いであり、シャッフルの偏りにつながります。

時間の複雑さはO(n)です。


8

このアルゴリズムは単純ですが効率的ではありません、O(N 2)。すべての「order by」アルゴリズムは通常O(N log N)です。おそらく何十万もの要素を下回っても違いはありませんが、大きなリストの場合は違います。

var stringlist = ... // add your values to stringlist

var r = new Random();

var res = new List<string>(stringlist.Count);

while (stringlist.Count >0)
{
   var i = r.Next(stringlist.Count);
   res.Add(stringlist[i]);
   stringlist.RemoveAt(i);
}

それがO(N 2)である理由は微妙です。最後から順番に削除しない限り、List.RemoveAt()はO(N)操作です。


2
これにはknuthシャッフルと同じ効果がありますが、1つのリストのデポピュレートと別のリストのポピュレーションが必要になるため、効率的ではありません。アイテムを適切な場所に交換することは、より良い解決策です。
Nick Johnson、

1
私はこのエレガントで簡単に理解できるものを見つけ、500の文字列では少しの違いはありません...
Sklivvz

4

Matt Howellsから拡張メソッドを作成することもできます。例。

   namespace System
    {
        public static class MSSystemExtenstions
        {
            private static Random rng = new Random();
            public static void Shuffle<T>(this T[] array)
            {
                rng = new Random();
                int n = array.Length;
                while (n > 1)
                {
                    int k = rng.Next(n);
                    n--;
                    T temp = array[n];
                    array[n] = array[k];
                    array[k] = temp;
                }
            }
        }
    }

その後、次のように使用できます。

        string[] names = new string[] {
                "Aaron Moline1", 
                "Aaron Moline2", 
                "Aaron Moline3", 
                "Aaron Moline4", 
                "Aaron Moline5", 
                "Aaron Moline6", 
                "Aaron Moline7", 
                "Aaron Moline8", 
                "Aaron Moline9", 
            };
        names.Shuffle<string>();

メソッドへのすべての呼び出しでrngを再作成するのはなぜですか?クラスレベルで宣言しますが、ローカルとして使用します...
Yaron

1

一連の文字列をシフトする必要があるため、配列のランダム化は集中的に行われます。配列からランダムに読み取るだけではどうですか。最悪の場合、getNextString()を使用してラッパークラスを作成することもできます。ランダム配列を作成する必要がある場合は、次のようにすることができます

for i = 0 -> i= array.length * 5
   swap two strings in random places

* 5は任意です。


配列からのランダムな読み取りは、いくつかのアイテムを複数回ヒットし、他のアイテムをミスする可能性があります!
レイ・ヘイズ

シャッフルアルゴリズムが壊れています。シャッフルが公平になる前に、実際に任意の5を非常に高くする必要があります。
ピタロウ2008

(整数)インデックスの配列を作成します。インデックスをシャッフルします。インデックスをそのランダムな順序で使用してください。重複、メモリ内の文字列参照のシャッフルはありません(それぞれがインターンをトリガーする場合とトリガーしない場合があります)。
クリストファー

1

私の頭のてっぺんを考えて、あなたはこれを行うことができます:

public string[] Randomize(string[] input)
{
  List<string> inputList = input.ToList();
  string[] output = new string[input.Length];
  Random randomizer = new Random();
  int i = 0;

  while (inputList.Count > 0)
  {
    int index = r.Next(inputList.Count);
    output[i++] = inputList[index];
    inputList.RemoveAt(index);
  }

  return (output);
}

0

同じ長さのランダムなfloatまたはintの配列を生成します。その配列をソートし、ターゲット配列で対応するスワップを実行します。

これにより、真に独立した並べ替えが実現します。


0
Random r = new Random();
List<string> list = new List(originalArray);
List<string> randomStrings = new List();

while(list.Count > 0)
{
int i = r.Random(list.Count);
randomStrings.Add(list[i]);
list.RemoveAt(i);
}

0

Jacco、あなたのソリューションはカスタムIComparerが安全ではありません。ソートルーチンは、正常に機能するために、コンパレータがいくつかの要件に準拠する必要があります。それらの最初に一貫性があります。同じオブジェクトのペアで比較演算子が呼び出された場合、常に同じ結果を返す必要があります。(比較は推移的でなければなりません)。

これらの要件を満たしていない場合、無限ループの可能性を含め、並べ替えルーチンで問題が発生する可能性があります。

ランダムな数値を各エントリに関連付けてからその値で並べ替えるソリューションに関しては、2つのエントリに同じ数値が割り当てられると出力のランダム性が損なわれるため、これらは出力に固有のバイアスをもたらします。(「安定した」ソートルーチンでは、入力の最初の方が出力の最初になります。Array.Sortはたまたま安定していませんが、クイックソートアルゴリズムによって実行されたパーティション分割に基づくバイアスがあります)。

必要なランダム性のレベルについて考えてみる必要があります。決定的な攻撃者から保護するために暗号レベルのランダム性が必要なポーカーサイトを運営している場合、曲のプレイリストをランダム化したいだけの人とは非常に異なる要件があります。

曲リストのシャッフルでは、シードされたPRNG(System.Randomなど)を使用しても問題はありません。ポーカーサイトの場合、これはオプションではなく、stackoverflowで誰もがあなたのためにやろうとするよりもずっと難しい問題について考える必要があります。(暗号化RNGの使用は始まりにすぎません。アルゴリズムがバイアスを導入しないこと、エントロピーの十分なソースがあること、およびその後のランダム性を損なう可能性のある内部状態を公開しないことを確認する必要があります)。


0

この投稿はすでにかなりよく回答されています-高速で公平な結果を得るには、フィッシャーイェーツのシャッフルのDurstenfeld実装を使用してください。いくつかの実装が投稿されていますが、実際には正しくないものもあります。

私はしばらく前に、この手法を使用して完全シャッフルと部分シャッフルを実装することについていくつか投稿しました(この2番目のリンクは、私が価値を追加したいと思っている場所です)、実装が公平であるかどうかを確認する方法についてのフォローアップ投稿、これは、任意のシャッフルアルゴリズムをチェックするために使用できます。2番目の投稿の最後で、乱数選択の単純な間違いの影響を確認できます。


1
あなたのリンクはまだ壊れています:/
Wai Ha Lee

0

わかりました、これは明らかに私の側からのバンプです(謝罪...)が、私はしばしば非常に一般的で暗号学的に強力な方法を使用します。

public static class EnumerableExtensions
{
    static readonly RNGCryptoServiceProvider RngCryptoServiceProvider = new RNGCryptoServiceProvider();
    public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
    {
        var randomIntegerBuffer = new byte[4];
        Func<int> rand = () =>
                             {
                                 RngCryptoServiceProvider.GetBytes(randomIntegerBuffer);
                                 return BitConverter.ToInt32(randomIntegerBuffer, 0);
                             };
        return from item in enumerable
               let rec = new {item, rnd = rand()}
               orderby rec.rnd
               select rec.item;
    }
}

Shuffle()は任意のIEnumerableの拡張機能であるため、リストでランダムな順序で0から1000までの数値を取得するには、

Enumerable.Range(0,1000).Shuffle().ToList()

ソート値はシーケンス内の要素ごとに1回だけ生成および記憶されるため、この方法でもソートに関しては驚くことはありません。


0

複雑なアルゴリズムは必要ありません。

たった1つの単純な行:

Random random = new Random();
array.ToList().Sort((x, y) => random.Next(-1, 1)).ToArray();

我々は変換する必要があることを注意ArrayListあなたが使用していない場合は、最初のList最初の場所で。

また、これは非常に大きな配列では効率的ではないことに注意してください!それ以外の場合は、クリーンでシンプルです。


エラー:演算子 '。' タイプ 'void'のオペランドには適用できません
有用なビー

0

これは、ここで提供される例に基づく完全に機能するコンソールソリューションです

class Program
{
    static string[] words1 = new string[] { "brown", "jumped", "the", "fox", "quick" };

    static void Main()
    {
        var result = Shuffle(words1);
        foreach (var i in result)
        {
            Console.Write(i + " ");
        }
        Console.ReadKey();
    }

   static string[] Shuffle(string[] wordArray) {
        Random random = new Random();
        for (int i = wordArray.Length - 1; i > 0; i--)
        {
            int swapIndex = random.Next(i + 1);
            string temp = wordArray[i];
            wordArray[i] = wordArray[swapIndex];
            wordArray[swapIndex] = temp;
        }
        return wordArray;
    }         
}

0
        int[] numbers = {0,1,2,3,4,5,6,7,8,9};
        List<int> numList = new List<int>();
        numList.AddRange(numbers);

        Console.WriteLine("Original Order");
        for (int i = 0; i < numList.Count; i++)
        {
            Console.Write(String.Format("{0} ",numList[i]));
        }

        Random random = new Random();
        Console.WriteLine("\n\nRandom Order");
        for (int i = 0; i < numList.Capacity; i++)
        {
            int randomIndex = random.Next(numList.Count);
            Console.Write(String.Format("{0} ", numList[randomIndex]));
            numList.RemoveAt(randomIndex);
        }
        Console.ReadLine();

-1

OLINQを使用する簡単な方法を次に示します。

// Input array
List<String> lst = new List<string>();
for (int i = 0; i < 500; i += 1) lst.Add(i.ToString());

// Output array
List<String> lstRandom = new List<string>();

// Randomize
Random rnd = new Random();
lstRandom.AddRange(from s in lst orderby rnd.Next(100) select s);

-2
private ArrayList ShuffleArrayList(ArrayList source)
{
    ArrayList sortedList = new ArrayList();
    Random generator = new Random();

    while (source.Count > 0)
    {
        int position = generator.Next(source.Count);
        sortedList.Add(source[position]);
        source.RemoveAt(position);
    }  
    return sortedList;
}

:私には、それはあなたが代わりに二番目の配列を宣言することによって、配列をシャッフルしようとすることで効率とreadabliltyの両方を高めることができるように、あなたがより良いリスト、シャッフルやArrayに戻っに変換しようと感じているsortedList = source.ToList().OrderBy(x => generator.Next()).ToArray();
T_D
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.