C#のList <T>からN個のランダムな要素を選択します


158

一般的なリストから5つのランダムな要素を選択するための簡単なアルゴリズムが必要です。たとえば、から5つのランダムな要素を取得しList<string>ます。


11
ランダムとは、包括的または排他的という意味ですか?IOW、同じ要素を複数回選択できますか?(本当にランダム)または、要素が選択されると、利用可能なプールからそれを選択できなくなりますか?
プレッツェル

回答:


127

反復し、要素ごとに選択の確率=(必要な数)/(残りの数)

したがって、40個のアイテムがある場合、最初のアイテムが5/40の確率で選択されます。そうである場合、次の確率は4/39であり、そうでない場合は5/39の確率です。あなたが最後に到達するときまでに、あなたはあなたの5つのアイテムを手に入れるでしょう、そしてしばしばあなたはそれの前にそれらのすべてを持っているでしょう。


33
これは微妙に間違っていると思います。リストのバックエンドはより大きな確率で表示されるため、リストのバックエンドはフロントエンドよりも頻繁に選択されるようです。たとえば、最初の35個の数字が選択されない場合、最後の5個の数字を選択する必要があります。最初の番号は5/40のチャンスしかありませんが、最後の番号は5/40回よりも頻繁に1/1を参照します。このアルゴリズムを実装する前に、まずリストをランダム化する必要があります。
Ankur Goel 2010

23
わかりました。このアルゴリズムを40個の要素のリストで1,000万回実行しました。各要素は、選択時に5/40(.125)のショットで撮影され、そのシミュレーションを数回実行しました。これは均等に分散されていないことがわかります。要素16から22は選択不足(16 = .123、17 = .124)になり、要素34は選択過剰(34 = .129)になります。要素39と40も選択不足になりますが、それほど多くはありません(39 = .1247、40 = .1246)
Goel

21
@Ankur:それが統計的に有意であるとは思いません。これが均等な分布を提供するという帰納的な証明があると私は信じています。
2010

9
同じトライアルを1億回繰り返しましたが、私のトライアルでは、最も頻繁に選択されたアイテムよりも0.106%未満の頻度で選択されたアイテムが選択されました。
再帰的

5
@recursive:証明はほとんど取るに足らないものです。どのKについても、KからKアイテムを選択する方法と、NからNから0アイテムを選択する方法を知っています。次に、確率K / Nの最初のアイテムを選択し、既知の方法を使用して残りのN-1からまだ必要なKまたはK-1アイテムを選択することで、NからKアイテムを選択できます。
Ilmari Karonen

216

linqの使用:

YourList.OrderBy(x => rnd.Next()).Take(5)

2
+1しかし、2つの要素がrnd.Next()などから同じ数を取得した場合、最初の要素が選択され、2番目の要素は選択されない可能性があります(これ以上要素が必要ない場合)。ただし、使用方法によっては十分にランダムです。
Lasse Espeholt、2010

7
順序はO(n log(n))だと思うので、コードの単純さが主な関心事(つまり、小さなリストの場合)であれば、このソリューションを選択します。
グイド

2
しかし、これはリスト全体を列挙してソートしませんか?OPが「迅速」でなくても、OPは「パフォーマンス」ではなく「簡単」を意味していました...
drzaus 2013年

2
これは、OrderBy()が要素ごとに1回だけキーセレクターを呼び出す場合にのみ機能します。2つの要素間の比較を実行したいときにいつでもそれを呼び出すと、毎回異なる値が返され、並べ替えが失敗します。[ドキュメント](msdn.microsoft.com/en-us/library/vstudio/…)は、どちらを行うかについては言及していません。
Oliver Bock 2015年

2
YourListアイテムがたくさんあるが、ほんのいくつかを選択したい場合は注意してください。この場合、効率的な方法ではありません。
Callum Watkins

39
public static List<T> GetRandomElements<T>(this IEnumerable<T> list, int elementsCount)
{
    return list.OrderBy(arg => Guid.NewGuid()).Take(elementsCount).ToList();
}

27

これは実際には思ったより難しい問題です。主に、数学的に正しい多くの解決策では、実際にすべての可能性を実現することができないためです(詳細は以下を参照)。

最初に、実装が簡単で、正しい場合は正しい乱数ジェネレータをいくつか示します。

(0)カイルの答え、O(n)。

(1)nペアのリスト[(0、rand)、(1、rand)、(2、rand)、...]を生成し、2番目の座標でソートし、最初のkを使用します(あなたのために、k = 5)ランダムなサブセットを取得するためのインデックス。これはO(n log n)時間ですが、実装は簡単だと思います。

(2)空のリストs = []を初期化して、k個のランダム要素のインデックスになるようにします。ランダムに{0、1、2、...、n-1}から数値rを選択し、r = rand%nとし、これをsに追加します。次に、r = rand%(n-1)を取り、sに固執します。衝突を回避するために、sでそれより少ない#要素をrに追加します。次に、r = rand%(n-2)を取り、sにk個の異なる要素があるまで、同じことを行います。これには最悪の場合の実行時間O(k ^ 2)があります。したがって、k << nの場合、これはより高速になる可能性があります。sをソートし続け、その連続する間隔を追跡する場合、O(k log k)で実装できますが、それはより多くの作業です。

@カイル-あなたは正しい、私はあなたの答えに同意する第二の考えで。私は最初にそれを急いで読み、誤って固定された確率k / nで各要素を順番に選択するように指示していると誤って考えましたが、あなたの適応アプローチは私には正しいようです。申し訳ありません。

さて、そして今キッカーのために:漸近的に(固定されたk、nの成長に対して)、n ^ k / kがあります!n個の要素からk個の要素のサブセットを選択[これは(n個のk個を選択)の近似です]。nが大きく、kがそれほど小さくない場合、これらの数値は巨大です。標準の32ビット乱数ジェネレータで期待できる最適なサイクル長は、2 ^ 32 = 256 ^ 4です。したがって、1000個の要素のリストがあり、ランダムに5個を選択したい場合、標準の乱数ジェネレーターがすべての可能性にヒットすることはありません。ただし、小さいセットで問題なく動作し、常にランダムに見える場合は、これらのアルゴリズムに問題はありません。

補遺:これを書いた後、アイデア(2)を正しく実装するのは難しいことに気付いたので、この答えを明確にしたいと思いました。O(k log k)時間を取得するには、O(log m)の検索と挿入をサポートする配列のような構造が必要です。バランスのとれたバイナリツリーでこれを行うことができます。このような構造を使用して、sという配列を作成します。ここにいくつかの擬似Pythonを示します。

# Returns a container s with k distinct random numbers from {0, 1, ..., n-1}
def ChooseRandomSubset(n, k):
  for i in range(k):
    r = UniformRandom(0, n-i)                 # May be 0, must be < n-i
    q = s.FirstIndexSuchThat( s[q] - q > r )  # This is the search.
    s.InsertInOrder(q ? r + q : r + len(s))   # Inserts right before q.
  return s

これが上記の英語の説明を効率的に実装する方法を確認するために、いくつかのサンプルケースを実行することをお勧めします。


2
(1)の場合は、並べ替えよりも速くリストをシャッフルできます。(2)の場合は、%
jk

あなたはRNGの周期長について提起異議を考えると、私たちはどのような方法があることができます等しい確率ですべてのセットを選択するアルゴリズムを構築するには?
ジョナ

(1)の場合、O(n log(n))を改善するには、選択ソートを使用してk個の最小要素を見つけることができます。これはO(n * k)で実行されます。
Jared

@ジョナ:そう思います。複数の独立した乱数ジェネレーターを組み合わせて、より大きなものを作成できると仮定しましょう(crypto.stackexchange.com/a/27431)。次に、問題のリストのサイズを処理するのに十分な大きさの範囲が必要です。
Jared

16

選んだ答えは正解でかなり甘いと思います。ただし、結果をランダムな順序で表示したかったため、別の方法で実装しました。

    static IEnumerable<SomeType> PickSomeInRandomOrder<SomeType>(
        IEnumerable<SomeType> someTypes,
        int maxCount)
    {
        Random random = new Random(DateTime.Now.Millisecond);

        Dictionary<double, SomeType> randomSortTable = new Dictionary<double,SomeType>();

        foreach(SomeType someType in someTypes)
            randomSortTable[random.NextDouble()] = someType;

        return randomSortTable.OrderBy(KVP => KVP.Key).Take(maxCount).Select(KVP => KVP.Value);
    }

驚くばかり!本当に助けてくれました!
Armstrongest

1
Environment.TickCountとDateTime.Now.Millisecondに基づく新しいRandom()を使用しない理由はありますか?
Lasse Espeholt、2010

いいえ、デフォルトが存在することを知らなかっただけです。
フランクSchwieterman

randomSortTableの改良:randomSortTable = someTypes.ToDictionary(x => random.NextDouble()、y => y); foreachループを保存します。
Keltex 2010

2
1年遅くても大丈夫ですが、これは@ersinのかなり短い答えにうまくいきません。繰り返しの乱数を取得しても失敗しません(繰り返しのペアの最初の項目に偏っています)。
Andiih、2011

12

私はこの問題に出くわしました、そしていくつかのより多くのグーグル検索はリストをランダムにシャッフルする問題に私を連れて行きました:http : //en.wikipedia.org/wiki/Fisher-Yates_shuffle

リストを完全にランダムに(インプレースで)シャッフルするには、次のようにします。

n要素の配列aをシャッフルするには(インデックス0..n-1):

  for i from n  1 downto 1 do
       j  random integer with 0  j  i
       exchange a[j] and a[i]

最初の5つの要素のみが必要な場合、n-1から1までiを実行する代わりに、n-5まで実行するだけです(つまり、n-5)。

k個のアイテムが必要だとしましょう。

これは次のようになります。

  for (i = n  1; i >= n-k; i--)
  {
       j = random integer with 0  j  i
       exchange a[j] and a[i]
  }

選択された各項目は配列の末尾に向かってスワップされるため、選択されたk個の要素は配列の最後のk個の要素です。

これにはO(k)の時間がかかります。ここで、kはランダムに選択された要素の数です。

さらに、初期リストを変更したくない場合は、すべてのスワップを一時リストに書き留め、そのリストを逆にして再度適用することで、スワップの逆のセットを実行し、変更せずに初期リストを返すことができます。 O(k)実行時間。

最後に、実際のスティックラーでは、(n == k)の場合、ランダムに選択された整数は常に0になるため、nkではなく1で停止する必要があります。


私はブログ投稿でC#を使用してそれを実装しました:vijayt.com/post/random-select-using-fisher-yates-algorithm。それがC#の方法を探している誰かを助けることを願っています。
vijayst 2017


8

Dragons in the Algorithmから、C#での解釈:

int k = 10; // items to select
var items = new List<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 });
var selected = new List<int>();
double needed = k;
double available = items.Count;
var rand = new Random();
while (selected.Count < k) {
   if( rand.NextDouble() < needed / available ) {
      selected.Add(items[(int)available-1])
      needed--;
   }
   available--;
}

このアルゴリズムは、アイテムリストの一意のインデックスを選択します。


リストで十分なアイテムのみを取得し、ランダムに取得しないでください。
culithay 2012年

2
この実装は、使用しているため壊れているvarで結果neededavailableなり、両方は整数で、needed/available常に0
ニコ

1
これは、受け入れられた回答の実装のようです。
DCシャノン2015年

6

グループからN個のランダムなアイテムを選択しても、順序とは関係ありません。ランダム性とは、予測不能性に関するものであり、グループ内のポジションをシャッフルすることではありません。ある種の順序付けを扱うすべての答えは、そうでないものよりも効率が悪くなるはずです。ここでは効率が鍵なので、アイテムの順番をあまり変えないものを掲載します。

1)真のランダム値が必要な場合、つまり、選択する要素に制限がない場合(つまり、選択したアイテムを再選択できる場合):

public static List<T> GetTrueRandom<T>(this IList<T> source, int count, 
                                       bool throwArgumentOutOfRangeException = true)
{
    if (throwArgumentOutOfRangeException && count > source.Count)
        throw new ArgumentOutOfRangeException();

    var randoms = new List<T>(count);
    randoms.AddRandomly(source, count);
    return randoms;
}

例外フラグをオフに設定すると、ランダムなアイテムを何度でも選択できます。

{1、2、3、4}がある場合、3つのアイテムに対して{1、4、4}、{1、4、3}など、または{1、4、3、2、4} 5アイテム!

チェックするものがないので、これはかなり高速になるはずです。

2)繰り返しなしでグループの個々のメンバーが必要な場合は、辞書を使用します(多くの人がすでに指摘しています)。

public static List<T> GetDistinctRandom<T>(this IList<T> source, int count)
{
    if (count > source.Count)
        throw new ArgumentOutOfRangeException();

    if (count == source.Count)
        return new List<T>(source);

    var sourceDict = source.ToIndexedDictionary();

    if (count > source.Count / 2)
    {
        while (sourceDict.Count > count)
            sourceDict.Remove(source.GetRandomIndex());

        return sourceDict.Select(kvp => kvp.Value).ToList();
    }

    var randomDict = new Dictionary<int, T>(count);
    while (randomDict.Count < count)
    {
        int key = source.GetRandomIndex();
        if (!randomDict.ContainsKey(key))
            randomDict.Add(key, sourceDict[key]);
    }

    return randomDict.Select(kvp => kvp.Value).ToList();
}

ここでは、コードを追加するだけでなくリストからも削除しているため、コードは他の辞書のアプローチよりも少し長くなっています。ここで、と等しくなったときに、何も並べ替えていないことがわかります。それは、ランダムさが全体として返されるセットに含まれるべきだと私が信じているからです。私はあなたがしたい場合意味5からランダムにアイテムを、それがその場合は問題ないはずかが、あなたが必要な場合は4つの同じセットから項目を、それは予測不可能で得られるはず、、、第二などのランダムなアイテムの数がするとき返されたものは元のグループの半分以上であり、削除する方が簡単ですcountsource.Count1, 2, 3, 4, 51, 3, 4, 2, 51, 2, 3, 4, 51, 2, 3, 41, 3, 5, 22, 3, 5, 4source.Count - countアイテムを追加するよりもグループからアイテムを追加しcountます。パフォーマンス上の理由から、removeメソッドでランダムなインデックスを取得するsource代わりに使用しsourceDictました。

したがって、{1、2、3、4}がある場合、これは3つのアイテムに対して{1、2、3}、{3、4、1}などになる可能性があります。

3)元のグループの重複を考慮に入れて、グループから完全に異なるランダムな値が必要な場合は、上記と同じ方法を使用できますがHashSet、辞書より軽量です。

public static List<T> GetTrueDistinctRandom<T>(this IList<T> source, int count, 
                                               bool throwArgumentOutOfRangeException = true)
{
    if (count > source.Count)
        throw new ArgumentOutOfRangeException();

    var set = new HashSet<T>(source);

    if (throwArgumentOutOfRangeException && count > set.Count)
        throw new ArgumentOutOfRangeException();

    List<T> list = hash.ToList();

    if (count >= set.Count)
        return list;

    if (count > set.Count / 2)
    {
        while (set.Count > count)
            set.Remove(list.GetRandom());

        return set.ToList();
    }

    var randoms = new HashSet<T>();
    randoms.AddRandomly(list, count);
    return randoms.ToList();
}

randoms変数が構成されているHashSet重複が最もまれな症例の最も稀で添加されることを回避するためにRandom.Next、入力リストが小さい場合は特に、同一の値を得ることができます。

したがって、{1、2、2、4} => 3つのランダムなアイテム=> {1、2、4}、決して{1、2、2}

{1、2、2、4} =>ランダムな4つのアイテム=>例外!! または、フラグセットに応じて{1、2、4}。

私が使用した拡張メソッドのいくつか:

static Random rnd = new Random();
public static int GetRandomIndex<T>(this ICollection<T> source)
{
    return rnd.Next(source.Count);
}

public static T GetRandom<T>(this IList<T> source)
{
    return source[source.GetRandomIndex()];
}

static void AddRandomly<T>(this ICollection<T> toCol, IList<T> fromList, int count)
{
    while (toCol.Count < count)
        toCol.Add(fromList.GetRandom());
}

public static Dictionary<int, T> ToIndexedDictionary<T>(this IEnumerable<T> lst)
{
    return lst.ToIndexedDictionary(t => t);
}

public static Dictionary<int, T> ToIndexedDictionary<S, T>(this IEnumerable<S> lst, 
                                                           Func<S, T> valueSelector)
{
    int index = -1;
    return lst.ToDictionary(t => ++index, valueSelector);
}

リスト内の何万ものアイテムのパフォーマンスがすべて10000回繰り返される必要がある場合は、ランダムクラスをより高速にする必要があるかもしれSystem.Randomませんが、後者がおそらく決してボトルネック、かなり高速

編集:返されたアイテムの順序も並べ替える必要がある場合、ダキムのフィッシャーイェイツのアプローチに勝るものはありません-短く、甘くてシンプルです。


6

(言い換え)に関する受け入れられた回答についての@JohnShedletskyによるコメントを考えていました:

O(originalList.Length)ではなく、O(subset.Length)でこれを実行できるはずです。

基本的に、subsetランダムなインデックスを生成し、元のリストからそれらを取得できるはずです。

メソッド

public static class EnumerableExtensions {

    public static Random randomizer = new Random(); // you'd ideally be able to replace this with whatever makes you comfortable

    public static IEnumerable<T> GetRandom<T>(this IEnumerable<T> list, int numItems) {
        return (list as T[] ?? list.ToArray()).GetRandom(numItems);

        // because ReSharper whined about duplicate enumeration...
        /*
        items.Add(list.ElementAt(randomizer.Next(list.Count()))) ) numItems--;
        */
    }

    // just because the parentheses were getting confusing
    public static IEnumerable<T> GetRandom<T>(this T[] list, int numItems) {
        var items = new HashSet<T>(); // don't want to add the same item twice; otherwise use a list
        while (numItems > 0 )
            // if we successfully added it, move on
            if( items.Add(list[randomizer.Next(list.Length)]) ) numItems--;

        return items;
    }

    // and because it's really fun; note -- you may get repetition
    public static IEnumerable<T> PluckRandomly<T>(this IEnumerable<T> list) {
        while( true )
            yield return list.ElementAt(randomizer.Next(list.Count()));
    }

}

さらに効率を高めたい場合は、実際のリスト要素ではなく、インデックスのを使用することHashSetになります(複雑な型や高価な比較がある場合)。

ユニットテスト

そして、衝突などがないことを確認します。

[TestClass]
public class RandomizingTests : UnitTestBase {
    [TestMethod]
    public void GetRandomFromList() {
        this.testGetRandomFromList((list, num) => list.GetRandom(num));
    }

    [TestMethod]
    public void PluckRandomly() {
        this.testGetRandomFromList((list, num) => list.PluckRandomly().Take(num), requireDistinct:false);
    }

    private void testGetRandomFromList(Func<IEnumerable<int>, int, IEnumerable<int>> methodToGetRandomItems, int numToTake = 10, int repetitions = 100000, bool requireDistinct = true) {
        var items = Enumerable.Range(0, 100);
        IEnumerable<int> randomItems = null;

        while( repetitions-- > 0 ) {
            randomItems = methodToGetRandomItems(items, numToTake);
            Assert.AreEqual(numToTake, randomItems.Count(),
                            "Did not get expected number of items {0}; failed at {1} repetition--", numToTake, repetitions);
            if(requireDistinct) Assert.AreEqual(numToTake, randomItems.Distinct().Count(),
                            "Collisions (non-unique values) found, failed at {0} repetition--", repetitions);
            Assert.IsTrue(randomItems.All(o => items.Contains(o)),
                        "Some unknown values found; failed at {0} repetition--", repetitions);
        }
    }
}

2
問題あり、いい考えです。(1)より大きなリストが巨大な場合(データベースから読み取るなど)、メモリ全体を超える可能性があるリスト全体を認識します。(2)KがNに近い場合、ループ内で要求されていないインデックスを検索するために大量のスラッシュが発生し、コードに予測できない時間を要します。これらの問題は解決可能です。
Paul Chernoch

1
スラッシングの問題に対する私の解決策は次のとおりです。K<N / 2の場合は、自分のやり方で行ってください。K> = N / 2の場合、保持する必要があるインデックスではなく、保持する必要がないインデックスを選択します。スラッシングはまだいくつかありますが、はるかに少ないです。
Paul Chernoch

これにより、列挙される項目の順序が変更されることにも注意してください。これは、状況によっては許容できる場合もありますが、状況によっては許容できない場合もあります。
Paul Chernoch

平均して、K = N / 2(Paulが提案する改善の最悪のケース)の場合、(スラッシングの改善)アルゴリズムは約0.693 * N回の反復をとるようです。次に速度比較を行います。これは受け入れられた答えよりも優れていますか?どのサンプルサイズについて?
mbomb007 2017

6

上記の回答のいくつかを組み合わせて、遅延評価された拡張メソッドを作成しました。私のテストは、カイルのアプローチ(Order(N))が、選択するランダムなインデックスを提案するためのdrzausのセットの使用(Order(K))よりも何倍も遅いことを示しました。前者は、乱数ジェネレーターに対してさらに多くの呼び出しを実行し、さらにアイテムに対してより多くの回数繰り返します。

私の実装の目標は:

1)IListではないIEnumerableが指定されている場合、完全なリストを実現しないでください。大量のアイテムのシーケンスが与えられた場合、メモリ不足になりたくありません。オンラインソリューションにはカイルのアプローチを使用してください。

2)それがIListであるとわかる場合は、ひねりを加えたdrzausのアプローチを使用します。KがNの半分よりも大きい場合、ランダムなインデックスを何度も選択し、それらをスキップする必要があるため、スラッシングのリスクがあります。したがって、保持しないインデックスのリストを作成します。

3)私は、アイテムが遭遇したのと同じ順序で返品されることを保証します。カイルのアルゴリズムは変更を必要としませんでした。drzausのアルゴリズムでは、ランダムなインデックスが選択される順序でアイテムを出力しないようにする必要がありました。すべてのインデックスをSortedSetに収集してから、ソートされたインデックス順にアイテムを出力します。

4)KがNと比較して大きく、セットの意味を反転させた場合、すべての項目を列挙し、インデックスがセットにないかどうかをテストします。これは、私はOrder(K)ランタイムを失うことを意味しますが、これらの場合、KはNに近いので、多くを失うことはありません。

これがコードです:

    /// <summary>
    /// Takes k elements from the next n elements at random, preserving their order.
    /// 
    /// If there are fewer than n elements in items, this may return fewer than k elements.
    /// </summary>
    /// <typeparam name="TElem">Type of element in the items collection.</typeparam>
    /// <param name="items">Items to be randomly selected.</param>
    /// <param name="k">Number of items to pick.</param>
    /// <param name="n">Total number of items to choose from.
    /// If the items collection contains more than this number, the extra members will be skipped.
    /// If the items collection contains fewer than this number, it is possible that fewer than k items will be returned.</param>
    /// <returns>Enumerable over the retained items.
    /// 
    /// See http://stackoverflow.com/questions/48087/select-a-random-n-elements-from-listt-in-c-sharp for the commentary.
    /// </returns>
    public static IEnumerable<TElem> TakeRandom<TElem>(this IEnumerable<TElem> items, int k, int n)
    {
        var r = new FastRandom();
        var itemsList = items as IList<TElem>;

        if (k >= n || (itemsList != null && k >= itemsList.Count))
            foreach (var item in items) yield return item;
        else
        {  
            // If we have a list, we can infer more information and choose a better algorithm.
            // When using an IList, this is about 7 times faster (on one benchmark)!
            if (itemsList != null && k < n/2)
            {
                // Since we have a List, we can use an algorithm suitable for Lists.
                // If there are fewer than n elements, reduce n.
                n = Math.Min(n, itemsList.Count);

                // This algorithm picks K index-values randomly and directly chooses those items to be selected.
                // If k is more than half of n, then we will spend a fair amount of time thrashing, picking
                // indices that we have already picked and having to try again.   
                var invertSet = k >= n/2;  
                var positions = invertSet ? (ISet<int>) new HashSet<int>() : (ISet<int>) new SortedSet<int>();

                var numbersNeeded = invertSet ? n - k : k;
                while (numbersNeeded > 0)
                    if (positions.Add(r.Next(0, n))) numbersNeeded--;

                if (invertSet)
                {
                    // positions contains all the indices of elements to Skip.
                    for (var itemIndex = 0; itemIndex < n; itemIndex++)
                    {
                        if (!positions.Contains(itemIndex))
                            yield return itemsList[itemIndex];
                    }
                }
                else
                {
                    // positions contains all the indices of elements to Take.
                    foreach (var itemIndex in positions)
                        yield return itemsList[itemIndex];              
                }
            }
            else
            {
                // Since we do not have a list, we will use an online algorithm.
                // This permits is to skip the rest as soon as we have enough items.
                var found = 0;
                var scanned = 0;
                foreach (var item in items)
                {
                    var rand = r.Next(0,n-scanned);
                    if (rand < k - found)
                    {
                        yield return item;
                        found++;
                    }
                    scanned++;
                    if (found >= k || scanned >= n)
                        break;
                }
            }
        }  
    } 

私は特別な乱数ジェネレータを使用していますが、必要に応じてC#の乱数を使用できます。(FastRandomはColin Greenによって作成され、SharpNEATの一部です。周期は2 ^ 128-1で、多くのRNGよりも優れています。)

単体テストは次のとおりです。

[TestClass]
public class TakeRandomTests
{
    /// <summary>
    /// Ensure that when randomly choosing items from an array, all items are chosen with roughly equal probability.
    /// </summary>
    [TestMethod]
    public void TakeRandom_Array_Uniformity()
    {
        const int numTrials = 2000000;
        const int expectedCount = numTrials/20;
        var timesChosen = new int[100];
        var century = new int[100];
        for (var i = 0; i < century.Length; i++)
            century[i] = i;

        for (var trial = 0; trial < numTrials; trial++)
        {
            foreach (var i in century.TakeRandom(5, 100))
                timesChosen[i]++;
        }
        var avg = timesChosen.Average();
        var max = timesChosen.Max();
        var min = timesChosen.Min();
        var allowedDifference = expectedCount/100;
        AssertBetween(avg, expectedCount - 2, expectedCount + 2, "Average");
        //AssertBetween(min, expectedCount - allowedDifference, expectedCount, "Min");
        //AssertBetween(max, expectedCount, expectedCount + allowedDifference, "Max");

        var countInRange = timesChosen.Count(i => i >= expectedCount - allowedDifference && i <= expectedCount + allowedDifference);
        Assert.IsTrue(countInRange >= 90, String.Format("Not enough were in range: {0}", countInRange));
    }

    /// <summary>
    /// Ensure that when randomly choosing items from an IEnumerable that is not an IList, 
    /// all items are chosen with roughly equal probability.
    /// </summary>
    [TestMethod]
    public void TakeRandom_IEnumerable_Uniformity()
    {
        const int numTrials = 2000000;
        const int expectedCount = numTrials / 20;
        var timesChosen = new int[100];

        for (var trial = 0; trial < numTrials; trial++)
        {
            foreach (var i in Range(0,100).TakeRandom(5, 100))
                timesChosen[i]++;
        }
        var avg = timesChosen.Average();
        var max = timesChosen.Max();
        var min = timesChosen.Min();
        var allowedDifference = expectedCount / 100;
        var countInRange =
            timesChosen.Count(i => i >= expectedCount - allowedDifference && i <= expectedCount + allowedDifference);
        Assert.IsTrue(countInRange >= 90, String.Format("Not enough were in range: {0}", countInRange));
    }

    private IEnumerable<int> Range(int low, int count)
    {
        for (var i = low; i < low + count; i++)
            yield return i;
    }

    private static void AssertBetween(int x, int low, int high, String message)
    {
        Assert.IsTrue(x > low, String.Format("Value {0} is less than lower limit of {1}. {2}", x, low, message));
        Assert.IsTrue(x < high, String.Format("Value {0} is more than upper limit of {1}. {2}", x, high, message));
    }

    private static void AssertBetween(double x, double low, double high, String message)
    {
        Assert.IsTrue(x > low, String.Format("Value {0} is less than lower limit of {1}. {2}", x, low, message));
        Assert.IsTrue(x < high, String.Format("Value {0} is more than upper limit of {1}. {2}", x, high, message));
    }
}

テストにエラーはありませんか?あなたが持ってif (itemsList != null && k < n/2)いるのは、内部if invertSetが常にfalseロジックが使用されないことを意味しています
NetMage

4

@ersの答えから拡張して、OrderByの異なる実装の可能性について心配している場合、これは安全なはずです。

// Instead of this
YourList.OrderBy(x => rnd.Next()).Take(5)

// Temporarily transform 
YourList
    .Select(v => new {v, i = rnd.Next()}) // Associate a random index to each entry
    .OrderBy(x => x.i).Take(5) // Sort by (at this point fixed) random index 
    .Select(x => x.v); // Go back to enumerable of entry

3

これは私が最初のカットで思いつくことができる最高のものです:

public List<String> getRandomItemsFromList(int returnCount, List<String> list)
{
    List<String> returnList = new List<String>();
    Dictionary<int, int> randoms = new Dictionary<int, int>();

    while (randoms.Count != returnCount)
    {
        //generate new random between one and total list count
        int randomInt = new Random().Next(list.Count);

        // store this in dictionary to ensure uniqueness
        try
        {
            randoms.Add(randomInt, randomInt);
        }
        catch (ArgumentException aex)
        {
            Console.Write(aex.Message);
        } //we can assume this element exists in the dictonary already 

        //check for randoms length and then iterate through the original list 
        //adding items we select via random to the return list
        if (randoms.Count == returnCount)
        {
            foreach (int key in randoms.Keys)
                returnList.Add(list[randoms[key]]);

            break; //break out of _while_ loop
        }
    }

    return returnList;
}

1-合計リスト数の範囲内でランダムなリストを使用し、リスト内のそれらのアイテムを単にプルするのが最善の方法のように思われましたが、一意性を確保するためにディクショナリを使用することは、まだ検討中です。

また、文字列リストを使用していることに注意してください。必要に応じて置き換えてください。


1
最初のショットで働いた!
サンガム2016年

3

私が使用する単純な解決策(おそらく大きなリストには適さない):リストを一時リストにコピーし、ループで一時リストからアイテムをランダムに選択し、一時リストから削除しながら選択リストに配置します(そうすることはできません)再選択されます)。

例:

List<Object> temp = OriginalList.ToList();
List<Object> selectedItems = new List<Object>();
Random rnd = new Random();
Object o;
int i = 0;
while (i < NumberOfSelectedItems)
{
            o = temp[rnd.Next(temp.Count)];
            selectedItems.Add(o);
            temp.Remove(o);
            i++;
 }

リストの真ん中から削除すると、コストがかかることがよくあります。非常に多くの削除が必要なアルゴリズムにリンクリストを使用することを検討してください。または同等に、削除されたアイテムをnull値に置き換えますが、既に削除されたアイテムを選択して再度選択する必要があるため、少しスラッシュします。
Paul Chernoch

3

ここで、John Shedletskyが指摘したように、アルゴリズムの複雑度がO(n)であるFisher-Yates Shuffleに基づく実装が1つあります。ここで、nはリストサイズではなく、サブセットまたはサンプルサイズです。

public static IEnumerable<T> GetRandomSample<T>(this IList<T> list, int sampleSize)
{
    if (list == null) throw new ArgumentNullException("list");
    if (sampleSize > list.Count) throw new ArgumentException("sampleSize may not be greater than list count", "sampleSize");
    var indices = new Dictionary<int, int>(); int index;
    var rnd = new Random();

    for (int i = 0; i < sampleSize; i++)
    {
        int j = rnd.Next(i, list.Count);
        if (!indices.TryGetValue(j, out index)) index = j;

        yield return list[index];

        if (!indices.TryGetValue(i, out index)) index = i;
        indices[j] = index;
    }
}

2

カイルの答えに基づいて、これが私のc#実装です。

/// <summary>
/// Picks random selection of available game ID's
/// </summary>
private static List<int> GetRandomGameIDs(int count)
{       
    var gameIDs = (int[])HttpContext.Current.Application["NonDeletedArcadeGameIDs"];
    var totalGameIDs = gameIDs.Count();
    if (count > totalGameIDs) count = totalGameIDs;

    var rnd = new Random();
    var leftToPick = count;
    var itemsLeft = totalGameIDs;
    var arrPickIndex = 0;
    var returnIDs = new List<int>();
    while (leftToPick > 0)
    {
        if (rnd.Next(0, itemsLeft) < leftToPick)
        {
            returnIDs .Add(gameIDs[arrPickIndex]);
            leftToPick--;
        }
        arrPickIndex++;
        itemsLeft--;
    }

    return returnIDs ;
}

2

この方法は、カイルの方法と同じかもしれません。

リストのサイズがnで、k要素が必要だとします。

Random rand = new Random();
for(int i = 0; k>0; ++i) 
{
    int r = rand.Next(0, n-i);
    if(r<k) 
    {
        //include element i
        k--;
    }
} 

魅力のように動作します:)

-アレックスギルバート


1
それは私と同等に見えます。同様のstackoverflow.com/a/48141/2449863と
DCShannon

1

なぜこのようなものではないのですか?

 Dim ar As New ArrayList
    Dim numToGet As Integer = 5
    'hard code just to test
    ar.Add("12")
    ar.Add("11")
    ar.Add("10")
    ar.Add("15")
    ar.Add("16")
    ar.Add("17")

    Dim randomListOfProductIds As New ArrayList

    Dim toAdd As String = ""
    For i = 0 To numToGet - 1
        toAdd = ar(CInt((ar.Count - 1) * Rnd()))

        randomListOfProductIds.Add(toAdd)
        'remove from id list
        ar.Remove(toAdd)

    Next
'sorry i'm lazy and have to write vb at work :( and didn't feel like converting to c#


1

目標:重複せずにコレクションソースからN個のアイテムを選択します。ジェネリックコレクションの拡張機能を作成しました。ここに私がそれをした方法があります:

public static class CollectionExtension
{
    public static IList<TSource> RandomizeCollection<TSource>(this IList<TSource> source, int maxItems)
    {
        int randomCount = source.Count > maxItems ? maxItems : source.Count;
        int?[] randomizedIndices = new int?[randomCount];
        Random random = new Random();

        for (int i = 0; i < randomizedIndices.Length; i++)
        {
            int randomResult = -1;
            while (randomizedIndices.Contains((randomResult = random.Next(0, source.Count))))
            {
                //0 -> since all list starts from index 0; source.Count -> maximum number of items that can be randomize
                //continue looping while the generated random number is already in the list of randomizedIndices
            }

            randomizedIndices[i] = randomResult;
        }

        IList<TSource> result = new List<TSource>();
        foreach (int index in randomizedIndices)
            result.Add(source.ElementAt(index));

        return result;
    }
}

0

私は最近、タイラーのポイント1に似たアイデアを使用してプロジェクトでこれを行いました。
たくさんの質問を読み込んで、ランダムに5つ選択しました。並べ替えはIComparerを使用して行われました。
aすべての質問がQuestionSorterリストに読み込まれ、リストのSort関数と最初に選択したk個の要素を使用してソートされました。

    private class QuestionSorter : IComparable<QuestionSorter>
    {
        public double SortingKey
        {
            get;
            set;
        }

        public Question QuestionObject
        {
            get;
            set;
        }

        public QuestionSorter(Question q)
        {
            this.SortingKey = RandomNumberGenerator.RandomDouble;
            this.QuestionObject = q;
        }

        public int CompareTo(QuestionSorter other)
        {
            if (this.SortingKey < other.SortingKey)
            {
                return -1;
            }
            else if (this.SortingKey > other.SortingKey)
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
    }

使用法:

    List<QuestionSorter> unsortedQuestions = new List<QuestionSorter>();

    // add the questions here

    unsortedQuestions.Sort(unsortedQuestions as IComparer<QuestionSorter>);

    // select the first k elements

0

これが私のアプローチです(全文はここhttp://krkadev.blogspot.com/2010/08/random-numbers-without-repetition.html)。

O(N)ではなくO(K)で実行する必要があります。Kは必要な要素の数、Nは選択するリストのサイズです。

public <T> List<T> take(List<T> source, int k) {
 int n = source.size();
 if (k > n) {
   throw new IllegalStateException(
     "Can not take " + k +
     " elements from a list with " + n +
     " elements");
 }
 List<T> result = new ArrayList<T>(k);
 Map<Integer,Integer> used = new HashMap<Integer,Integer>();
 int metric = 0;
 for (int i = 0; i < k; i++) {
   int off = random.nextInt(n - i);
   while (true) {
     metric++;
     Integer redirect = used.put(off, n - i - 1);
     if (redirect == null) {
       break;
     }
     off = redirect;
   }
   result.add(source.get(off));
 }
 assert metric <= 2*k;
 return result;
}

0

これは、受け入れられているソリューションほどエレガントでも効率的でもありませんが、書くのは簡単です。まず、配列をランダムに並べ替えてから、最初のK個の要素を選択します。Pythonでは、

import numpy

N = 20
K = 5

idx = np.arange(N)
numpy.random.shuffle(idx)

print idx[:K]

0

拡張メソッドを使用します。

    public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> elements, int countToTake)
    {
        var random = new Random();

        var internalList = elements.ToList();

        var selected = new List<T>();
        for (var i = 0; i < countToTake; ++i)
        {
            var next = random.Next(0, internalList.Count - selected.Count);
            selected.Add(internalList[next]);
            internalList[next] = internalList[internalList.Count - selected.Count];
        }
        return selected;
    }

0
public static IEnumerable<T> GetRandom<T>(this IList<T> list, int count, Random random)
    {
        // Probably you should throw exception if count > list.Count
        count = Math.Min(list.Count, count);

        var selectedIndices = new SortedSet<int>();

        // Random upper bound
        int randomMax = list.Count - 1;

        while (selectedIndices.Count < count)
        {
            int randomIndex = random.Next(0, randomMax);

            // skip over already selected indeces
            foreach (var selectedIndex in selectedIndices)
                if (selectedIndex <= randomIndex)
                    ++randomIndex;
                else
                    break;

            yield return list[randomIndex];

            selectedIndices.Add(randomIndex);
            --randomMax;
        }
    }

メモリー:
〜count複雑度:O(count 2


0

Nが非常に大きい場合、N個の数値をランダムにシャッフルして、たとえば最初のk個の数値を選択する通常の方法は、スペースが複雑なために禁止されます。次のアルゴリズムは、時間と空間の両方の複雑さに対してO(k)のみを必要とします。

http://arxiv.org/abs/1512.00501

def random_selection_indices(num_samples, N):
    modified_entries = {}
    seq = []
    for n in xrange(num_samples):
        i = N - n - 1
        j = random.randrange(i)

        # swap a[j] and a[i] 
        a_j = modified_entries[j] if j in modified_entries else j 
        a_i = modified_entries[i] if i in modified_entries else i

        if a_i != j:
            modified_entries[j] = a_i   
        elif j in modified_entries:   # no need to store the modified value if it is the same as index
            modified_entries.pop(j)

        if a_j != i:
            modified_entries[i] = a_j 
        elif i in modified_entries:   # no need to store the modified value if it is the same as index
            modified_entries.pop(i)
        seq.append(a_j)
    return seq

0

大きなリストでLINQを使用する(各要素を処理するのにコストがかかる場合)、および重複の可能性に対処できる場合:

new int[5].Select(o => (int)(rnd.NextDouble() * maxIndex)).Select(i => YourIEnum.ElementAt(i))

私の用途では、100.000要素のリストがあり、それらがDBからプルされたため、リスト全体のrndに比べて時間を約半分(またはそれ以上)にしました。

リストが大きいと、重複する確率が大幅に減少します。


このソリューションには繰り返し要素がある可能性があります!! 穴リストのランダムはそうではないかもしれません。
AxelWass 16

うーん。そうだね。私がそれをどこで使うかは問題ではありません。それを反映するように回答を編集しました。
Wolf5 16

-1

これはあなたの問題を解決します

var entries=new List<T>();
var selectedItems = new List<T>();


                for (var i = 0; i !=10; i++)
                {
                    var rdm = new Random().Next(entries.Count);
                        while (selectedItems.Contains(entries[rdm]))
                            rdm = new Random().Next(entries.Count);

                    selectedItems.Add(entries[rdm]);
                }

これは質問に答える可能性がありますが、このコードブロックが質問にどのように答えるかの説明を含めるように答えを編集する必要があります。これは、コンテキストを提供するのに役立ち、あなたの答えを将来の読者にとってはるかに有用にします。
Hoppeduppeanut
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.