C#でリストから複数の要素をランダムに重複せず取得する

C#であるリスト(IList)の中から重複しない複数の要素をランダムに取得します。

「任意のデータが入ったListを対象にランダムに値を取得でき」、操作後に「対象のリストは抽出前と後で変化しない」ことを条件とします。

Unityの場合、Random.Next(N, M) を Random.Range(N, M) -1 に置き換えてれば同じような結果が得られると思います(しらんけど。

以下コード例です。

// Prmgram.cs
internal static void Main(string[] args)
{
    // 抽出対象のリストを作成する
    //  → 100 ~ 109までの10個の値が入ってる
    IList<int> source = Enumerable.Range(100, 10).ToList();
    
    // 全部の値から4つ値を取得する
    foreach (int index in GetRandomIndex(0, source.Count() - 1, 4))
    {
        Console.WriteLine(source[index]);
        // 100
        // 106
        // 104
        // 108
    }

    // 指定した範囲内(5~10要素内{=104~109})から3つ値を取得する
    foreach (int index in GetRandomIndex(4/*5-1*/, 9/*10-1*/, 3))
    {
        Console.WriteLine(source[index]);
        // 106
        // 109
        // 108
    }
}

// 指定した範囲内から重複しないインデックスを作成する
public static IEnumerable<int> GetRandomIndex(int min, int max, int count)
{
    var random = new Random();

    var indexList = new List<int>();
    for (int i = min; i <= max; i++) // 最大値を含む
    {
        indexList.Add(i);
    }

    for (int i = 0; i < count; i++)
    {
        int index = random.Next(0, indexList.Count);
        int value = indexList[index];
        indexList.RemoveAt(index);
        yield return value;
    }
}

もともとあるリストに対して重複しないランダムなインデックスを生成し、そのインデックスでリストにアクセスします。

あまり無いと思いますが、リストの範囲を指定したランダム取り出しも可能です。

順次取り出しを行うので嫌になったら途中でループをbreakして処理を打ち切れます。

ただし、この方法だと事前に要素個数分のインデックスリストを作成するため、Listの要素数が10万など大量にある要素から3つ要素をと取り出すとめちゃくちゃ効率が悪いです。

その場合素直にRandomクラスでインデックスを作成してもし重複したら再抽選としたほうが処理が軽いと思います。

もっと簡単に取得したい

やっぱりリストに対して直接操作ができたほうが使う法としては簡単なので、IListに対して拡張メソッドを作成します。

// ListExtension.cs

using System.Linq.MyExtension;

namespace System.Linq.Extension
{
    public static class ListExtension
    {
        public static IEnumerable<T> GetRandom<T>(this IList<T> list, int count)
        {
            var random = new Random();

            var indexList = new List<int>();
            for (int i = 0; i < list.Count; i++)
            {
                indexList.Add(i);
            }

            for (int i = 0; i < count; i++)
            {
                int index = random.Next(0, indexList.Count);
                int value = indexList[index];
                indexList.RemoveAt(index);
                yield return list[value];
            }
        }
    }
}

そうすると以下のように使えるようになります。

// Prmgram.cs

using System.Linq.MyExtension;

internal static void Main(string[] args)
{
    // 抽出対象のリストを作成する
    //  → 100 ~ 109までの10個の値が入ってる
    IList<int> source = Enumerable.Range(100, 10).ToList();
    
    foreach (int value in source.GetRandom(3))
    {
        Console.WriteLine(value);
        // > 101
        // > 104
        // > 107
    }
}

以上です。