[C#] Random 및 OrderBy를 사용하는 것이 좋은 셔플 알고리즘입니까?

Coding Horror 에서 다양한 셔플 알고리즘에 대한 기사 를 읽었습니다 . 사람들이 목록을 섞기 위해이 작업을 수행 한 곳을 보았습니다.

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

이것이 좋은 셔플 알고리즘입니까? 정확히 어떻게 작동합니까? 이것이 허용되는 방법입니까?



답변

내가 좋아하는 셔플 방법이 아닙니다. 주로 O (n log n)이기 때문에 O (n) 셔플을 쉽게 구현 할 수있는 좋은 이유가 없습니다. 문제의 코드는 기본적으로 각 요소에 임의의 (희망적으로 고유 한) 숫자를 부여한 다음 해당 숫자에 따라 요소를 정렬하여 “작동”합니다.

나는 Durstenfield의 Fisher-Yates 셔플 변형을 선호 합니다.

간단한 Shuffle확장 방법을 구현하는 것은 기본적으로 Fisher-Yates의 기존 구현을 사용하여 호출 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대략 같은 시간에 두 개의 인스턴스를 생성 하면 동일한 방식으로 사용될 때 동일한 난수 시퀀스가 ​​생성됩니다.
  • Random 스레드 안전하지 않습니다.

나는이 에 대한 기사Random 이러한 문제에 대한 자세한 내용이수록 및 솔루션을 제공하는합니다.


답변

이것은 Jon Skeet의 답변을 기반으로 합니다.

이 답변에서 배열은 뒤섞이고를 사용하여 반환됩니다 yield. 결과적으로 배열은 foreach 기간 동안 반복적으로 필요한 객체뿐만 아니라 메모리에 유지되지만 비용은 모두 처음에 있습니다. 수율은 기본적으로 빈 루프입니다.

이 알고리즘은 게임에서 많이 사용되며 처음 세 항목을 선택하고 다른 항목은 나중에 필요한 경우에만 필요합니다. 내 제안은 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];
}


답변

이 Skeet 인용에서 시작 :

내가 좋아하는 셔플 방법이 아닙니다. 주로 O (n log n)이기 때문에 O (n) 셔플을 쉽게 구현 할 수있는 좋은 이유가 없습니다. 문제의 코드는 기본적으로 각 요소에 임의의 ( 희망적으로 고유 한 ) 숫자를 부여한 다음 해당 숫자에 따라 요소를 정렬하여 “작동”합니다 .

희망적으로 독특한 이유를 조금 설명 하겠습니다!

이제 Enumerable.OrderBy에서 :

이 방법은 안정적인 정렬을 수행합니다. 즉, 두 요소의 키가 동일하면 요소의 순서가 유지됩니다.

이건 매우 중요합니다! 두 요소가 동일한 난수를 “수신”하면 어떻게됩니까? 배열에서와 동일한 순서로 유지됩니다. 자, 이것이 일어날 가능성은 무엇입니까? 정확하게 계산하기는 어렵지만 생일 문제 는 바로이 문제입니다.

자, 진짜입니까? 사실인가요?

항상 그렇듯이 의심스러운 경우 몇 줄의 프로그램을 작성하십시오. http://pastebin.com/5CDnUxPG

이 작은 코드 블록은 Fisher-Yates 알고리즘을 뒤로 사용하고 Fisher-Yates 알고리즘을 앞으로 사용하여 3 번의 요소 배열을 특정 횟수만큼 섞습니다 ( wiki 페이지에는 두 개의 의사 코드 알고리즘이 있습니다 … 결과는하지만, 하나가 다른 하나는 제 마지막 요소에서 수행되는 동안), 마지막 요소로부터 제의 순 잘못된 알고리즘 수행 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 요소)까지 배열을 정렬 할 수 있어야한다고 말할 것입니다.


답변

대부분의 경우 probablly ok이며 거의 항상 진정한 임의 분포를 생성합니다 (Random.Next ()가 두 개의 동일한 임의 정수를 생성하는 경우 제외).

시리즈의 각 요소에 임의의 정수를 할당 한 다음이 정수로 순서를 정렬하여 작동합니다.

응용 프로그램의 99.9 %에 대해 완전히 허용됩니다 (위의 경우를 처리해야하는 경우 제외). 또한 런타임에 대한 skeet의 이의 제기가 유효하므로 긴 목록을 섞을 경우 사용하지 않을 수 있습니다.


답변

이것은 여러 번 전에 나타났습니다. StackOverflow에서 Fisher-Yates를 검색하십시오.

이 알고리즘을 위해 작성한 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;
                }
        }
}


답변

성능에 너무 걱정하지 않으면 좋은 셔플 링 알고리즘처럼 보입니다. 내가 지적한 유일한 문제는 그 동작을 제어 할 수 없다는 것이므로 테스트하기가 어려울 수 있습니다.

하나의 가능한 옵션은 시드를 난수 생성기 (또는 난수 생성기를 매개 변수)에 매개 변수로 전달하는 것이므로보다 쉽게 ​​제어하고 테스트 할 수 있습니다.


답변

Jon Skeet의 답변이 전적으로 만족스러운 것으로 나타 났지만 고객의 로보 스캐너는 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];
    }
}