【C#】重ならない乱数配列の作成

2024年2月21日

トランプのシャッフルなど、シャッフルをするのですが重なってはいけないケースの場合を考えてみましょう

C#の基本文法だけでも作成できますが、今回は一足飛びにLINQを使った方法を紹介します

0から9までの10個の整数をシャッフルするコード

元になるコード

int[] randomNumbers = Enumerable.Range(0, 10).OrderBy(_ => Guid.NewGuid()).ToArray();

0から9までの数字をランダムに並び替えた整数型の配列を作成します。

まず、Enumerable.Range(0, 10)は、0から9までの整数のシーケンスを作成します。

OrderBy(_ => Guid.NewGuid())は、ランダムなGUID(グローバル一意識別子)を使用して、配列の要素をランダムに並び替えます。

最後に、ToArray()メソッドを使用して、並び替えられたシーケンスを整数型の配列に変換します。

テスト用コード

int[] randomNumbers = Enumerable.Range(0, 10).OrderBy(_ => Guid.NewGuid()).ToArray();

foreach (var number in randomNumbers)
{
    Console.WriteLine(number);
}

結果

0
8
1
2
7
3
9
5
4
6

正数配列を渡して、シャッフルした結果を得るコード

元になるコード

int[] randomNumbers = numbers.OrderBy(_ => Guid.NewGuid()).ToArray();

このコードは、配列numbers内の要素をランダムな順序で並び替えるために使用されます。

OrderByメソッドは、LINQ(Language Integrated Query)によって提供される拡張メソッドの一種であり、IEnumerable<T>型のオブジェクトに対して使用されます。このメソッドは、指定されたキーに基づいてシーケンスの要素を昇順に並べ替えます。この例では、キーとしてランダムなGuidオブジェクトを使用しています。

Guid.NewGuid()は、ランダムなGUID(Globally Unique Identifier)を生成するための.NET Frameworkのメソッドです。

最後に、ToArray()メソッドは、結果を新しい配列として返します。randomNumbers変数は、ランダムに並び替えられたnumbersの要素が格納された新しい配列を表します。

テスト用コード

int[] numbers = new int[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };

int[] randomNumbers = numbers.OrderBy(_ => Guid.NewGuid()).ToArray();

foreach (var number in randomNumbers)
{
    Console.WriteLine(number);
}

結果

11
10
18
13
17
15
14
12
16
19

メソッドにしたテスト用コード

int[] numbers = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];

int[] randomNumbers = ShuffleRange(numbers);

foreach (var number in randomNumbers)
{
    Console.WriteLine(number);
}

/// <summary>
/// 与えれたた正数配列をシャッフルします
/// </summary>
/// <param name="array">シャッフルする正数配列</param>
/// <returns>シャッフルされた正数配列</returns>
int[] ShuffleRange(int[] array)
{
    return array.OrderBy(_ => Guid.NewGuid()).ToArray();
}

結果

15
16
18
19
13
10
17
11
14
12

トランプのカードをシャッフルするサンプル

トランプのカードクラス

class Card
{
    public enum SuitEnum
    {
        Spade,
        Heart,
        Diamond,
        Club,
    }

    public Card(SuitEnum suit, int number)
    {
        Suit = suit;
        Number = number;
    }

    public SuitEnum Suit { get; set; }
    public int Number { get; set; }
}

カードのインスタンスを作成する

4つのマークで1-13までのカードを作成することとします

Card[] cards = new Card[52];

int counter = 0;

for (int number = 1; number <= 13; number++)
{
    for (int suit = 0; suit < 4; suit++)
    {
        Card.SuitEnum suitEnum = (Card.SuitEnum)suit;
        cards[counter++] = new Card(suitEnum, number);
    }
}

カードのインスタンス配列をシャッフルするジェネリックメソッド

using System;
using System.Linq;

/// <summary>
/// 与えられたインスタンス配列をシャッフルします。
/// </summary>
/// <typeparam name="T">配列に含まれる任意の型の項目。</typeparam>
/// <param name="array">シャッフルするインスタンス配列。</param>
/// <returns>シャッフルされたインスタンス配列。</returns>
T[] ShuffleRange<T>(T[] array)
{
    return array.OrderBy(_ => Guid.NewGuid()).ToArray();
}

インスタンスを作成したカードのインスタンス(山札)をシャッフルするためのメソッド呼び出し

Card[] shuffleCards = ShuffleRange(cards);

テスト用コード

foreach (var card in shuffleCards)
{
    Console.WriteLine($"{card.Suit} {card.Number}");
}

結果

Club 5
Club 6
Spade 7
Heart 4
Diamond 3
Heart 8
Spade 11
Club 11
Diamond 9
Heart 6
Diamond 5
Diamond 1
Diamond 8
Club 3
Diamond 4
Spade 12
Spade 4
Heart 2
Club 9
Heart 3
Club 13
Club 8
Diamond 6
Diamond 2
Spade 13
Club 10
Spade 10
Spade 9
Heart 10
Heart 7
Heart 5
Heart 1
Club 2
Spade 5
Heart 11
Diamond 12
Heart 12
Heart 13
Club 1
Club 12
Diamond 10
Spade 8
Spade 2
Diamond 7
Club 4
Heart 9
Diamond 13
Club 7
Diamond 11
Spade 3
Spade 1
Spade 6

トランプのシャッフルコード(Listバージョン)

List<Card> cards = new List<Card>();


int counter = 0;

for (int number = 1; number <= 13; number++)
{
    for (int suit = 0; suit < 4; suit++)
    {
        Card.SuitEnum suitEnum = (Card.SuitEnum)Enum.ToObject(typeof(Card.SuitEnum), suit);
        cards.Add(new Card(suitEnum, number));
    }
}

List<Card> shuffleCars = ShuffleRange(cards);
/// <summary>
/// 与えれたたインスタンス配列をシャッフルします
/// </summary>
/// <param name="array">シャッフルする正数配列</param>
/// <returns>シャッフルされたインスタンス配列</returns>
List<T> ShuffleRange<T>(List<T> array) => array.OrderBy(_ => Guid.NewGuid()).ToList();

foreach (var card in shuffleCars)
{
    Console.WriteLine($"{card.Suit} {card.Number}");
}

class Card
{
    public enum SuitEnum
    {
        Spade,
        Heart,
        Diamond,
        Club,
    }

    public Card(SuitEnum suit, int number)
    {
        Suit = suit;
        Number = number;
    }

    public SuitEnum Suit { get; set; }
    public int Number { get; set; }
}

参考)

Guid.NewGuid()とは

Guid.NewGuid()は、.NET Framework、.NET Core、および.NETにおけるメソッドで、新しい一意のグローバルユニーク識別子(GUID)を生成します。GUIDは、128ビットの長さを持ち、通常、32文字の16進数で表され、ハイフンで区切られた5つのグループに分かれています(例:12345678-1234-1234-1234-123456789abc)。これらの識別子は、データベースのレコード、システムコンポーネントの識別、トランザクションの追跡など、多岐にわたる用途で使用されます。

GUIDは、その生成アルゴリズムにより、世界中でユニーク(またはその可能性が非常に高い)であることが保証されています。これは、特定のマシンやアプリケーション内だけでなく、異なるシステム間でも識別子の重複が極めて少ないことを意味します。

Guid.NewGuid()メソッドを使用すると、新しいGUID値を簡単に生成できます。このメソッドは、System.Guid型の新しいインスタンスを返します。以下は、C#での使用例です:

Guid myNewGuid = Guid.NewGuid();
Console.WriteLine(myNewGuid);

配列とListの両方に対応できるメソッドにするには

ShuffleRangeメソッドを次のように変更します
配列もListもIEnumerableインターフェースを実装しているためこのように更新できます

/// <summary>
/// 与えられたコレクションをシャッフルします
/// </summary>
/// <param name="collection">シャッフルするコレクション</param>
/// <returns>シャッフルされたコレクション</returns>
IEnumerable<T> ShuffleRange<T>(IEnumerable<T> collection)
{
    return collection.OrderBy(_ => Guid.NewGuid());
}

配列、リスト、ディクショナリの各コレクションを含めたサンプル

int[] lists = [1, 2, 3, 4, 5];

var shuffleArray = ShuffleRange(lists);

Console.WriteLine("配列の場合・・・");
foreach (var sArray in shuffleArray)
{
    Console.WriteLine(sArray);
}

List<int> list = [1, 2, 3, 4, 5];

var shuffleLists = ShuffleRange(list);

Console.WriteLine("リストの場合・・・");
foreach (var item in shuffleLists)
{
    Console.WriteLine(item);
}

Dictionary<int, int> dics = new Dictionary<int, int>
{
    [1] = 10,
    [2] = 20,
    [3] = 30,
    [4] = 40,
    [5] = 50,
};

var shuffleDics = ShuffleRange(dics).ToDictionary(pair => pair.Key, pair => pair.Value);

Console.WriteLine("ディクショナリの場合・・・");
foreach (var sDics in shuffleDics)
{
    Console.WriteLine(sDics);
}

/// <summary>
/// 与えられたコレクションをシャッフルします
/// </summary>
/// <param name="collection">シャッフルするコレクション</param>
/// <returns>シャッフルされたコレクション</returns>
IEnumerable<T> ShuffleRange<T>(IEnumerable<T> collection)
{
    return collection.OrderBy(_ => Guid.NewGuid());
}

実行結果のサンプル(実行ごとに変わります)

配列の場合・・・
5
1
4
2
3
リストの場合・・・
4
1
2
3
5
ディクショナリの場合・・・
[3, 30]
[1, 10]
[5, 50]
[4, 40]
[2, 20]

シャッフルのアルゴリズムを変えてみる

IEnumerable<T> ShuffleRange<T>(IEnumerable<T> collection) メソッドを Guid を使わない別のアルゴリズムで実装することを考えます。より一般的なアプローチの一つに、Fisher-Yates shuffle(またはKnuth shuffleとも呼ばれる)があります。このアルゴリズムは、配列やリストをインプレース(追加のメモリを使用せずに)でランダムにシャッフルします。ただし、IEnumerable<T> はその性質上、インプレースでの変更がサポートされていないため、まずコレクションをリストに変換し、そのリストをシャッフルしてから結果を返す必要があります。

以下に、Fisher-Yatesアルゴリズムを使用してIEnumerable<T> をシャッフルするメソッドの実装例を示します。この実装では、まず入力された IEnumerable<T> をリストに変換し、その後でランダムにシャッフルします。

IEnumerable<T> ShuffleRange<T>(IEnumerable<T> collection)
{
    Random random = new Random();

    List<T> list = collection.ToList();
    int n = list.Count;
    for (int i = 0; i < n; i++)
    {
        // Fisher-Yates shuffle の実装
        int r = i + random.Next(n - i);
        T temp = list[i];
        list[i] = list[r];
        list[r] = temp;
    }
    return list;
}

この実装では、まずIEnumerable<T>からList<T>への変換を行い、そのリストをシャッフルしています。Random インスタンスはクラスレベルで1つだけ生成して再利用することで、パフォーマンスを向上させています。また、Fisher-Yatesアルゴリズムを正確に実装することで、全ての要素が等しい確率で任意の位置に来るようにシャッフルされます。

コード解説

このコードは、与えられたコレクション(IEnumerable<T>型)の要素をシャッフル(ランダムに並び替え)するメソッドShuffleRangeの実装です。ジェネリック型Tを使用しているため、このメソッドは任意の型の要素を含むコレクションに対して動作します。ここで実装されているアルゴリズムは、Fisher-Yatesシャッフルと呼ばれるもので、効率的にかつ公平にコレクションをシャッフルすることができます。

手順は以下の通りです:

  1. まず、引数として渡されたコレクションをList<T>に変換し、これを操作可能なリストとして使用します。IEnumerable<T>は単に列挙可能なコレクションであり、直接要素の並び替えなどの操作はできないためです。
  2. Randomクラスのインスタンスを生成します。これを使ってランダムな数値を生成し、シャッフルの際にどの要素を交換するかを決定します。
  3. リストの各要素に対してループを実行します。このループ内で、現在の要素のインデックス(i)と、iからリストの末尾までの範囲内でランダムに選ばれたインデックス(r)の要素を交換します。random.Next(n - i)は、0から(n - i)未満のランダムな数値を生成し、これにiを加えることで、iからリストの末尾までの範囲でランダムなインデックスを得ます。
  4. このプロセスをリストの全要素に対して行うことで、全ての要素がランダムに並び替えられます。
  5. 最後に、シャッフルされたリストを返します。

このアルゴリズムは非常に効率的であり、任意のコレクションの要素を公平にランダムに並び替えることができます。Fisher-Yatesシャッフルは、元の配列を破壊的に変更する点に注意が必要ですが、この実装では元のコレクションは変更せず、新たにシャッフルされたリストを作成して返しています。

C#

Posted by hidepon