Coding Horror 에서 다양한 셔플 알고리즘에 대한 기사 를 읽었습니다 . 사람들이 목록을 섞기 위해이 작업을 수행 한 곳을 보았습니다.
var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());
이것이 좋은 셔플 알고리즘입니까? 정확히 어떻게 작동합니까? 이것이 허용되는 방법입니까?
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
이러한 문제에 대한 자세한 내용이수록 및 솔루션을 제공하는합니다.
source.ToArray();
에 있어야 using System.Linq;
합니다. 그렇지 않으면 다음과 같은 오류가 발생합니다.'System.Collections.Generic.IEnumerable<T>' does not contain a definition for 'ToArray' and no extension method 'ToArray' accepting a first argument of type 'System.Collections.Generic.IEnumerable<T>' could be found (are you missing a using directive or an assembly reference?)
이것은 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 요소)까지 배열을 정렬 할 수 있어야한다고 말할 것입니다.
이것은 여러 번 전에 나타났습니다. 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;
}
}
}
Random
이와 같은 정적 변수로 사용해서는 안됩니다 - Random
스레드 안전하지 않습니다. csharpindepth.com/Articles/Chapter12/Random.aspx
Random
내 기사에서 언급했듯이 사용하기가 어렵습니다.
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];
}
}
알고리즘을 찾고 계십니까? 내 ShuffleList
수업을 사용할 수 있습니다 .
class ShuffleList<T> : List<T>
{
public void Shuffle()
{
Random random = new Random();
for (int count = Count; count > 0; count--)
{
int i = random.Next(count);
Add(this[i]);
RemoveAt(i);
}
}
}
그런 다음 다음과 같이 사용하십시오.
ShuffleList<int> list = new ShuffleList<int>();
// Add elements to your list.
list.Shuffle();
5 개의 첫 정수의 초기 정렬 목록을 보자 : { 0, 1, 2, 3, 4 }
.
이 메소드는 요소의 숫자를 세면서 시작하여 호출합니다 count
. 그 다음으로 count
, 각 단계에서 감소 그 사이의 난수를 얻어 0
및 count
상기리스트의 끝으로 이동한다.
다음 단계별 예제에서 이동할 수있는 항목은 기울임 꼴 이며 선택한 항목은 굵게 표시됩니다 .
0 1 2 3 4
0 1 2 3 4
0 1 2 4 3
0 1 2 4 3
1 2 4 3 0
1 2 4 3 0
1 2 3 0 4
1 2 3 0 4
2 3 0 4 1
2 3 0 4 1
3 0 4 1 2
"이 알고리즘은 목록의 각 값에 대해 새로운 임의의 값을 생성 한 다음 임의의 값을 기준으로 목록을 정렬하여 셔플"과 같은 많은 답변이 매우 잘못되었다고 말할 것입니다!
나는 이것이 소스 컬렉션의 각 요소에 임의의 값을 할당하지 않는다고 생각합니다. 대신 Quicksort처럼 실행되는 정렬 알고리즘이있을 수 있습니다.이 알고리즘은 대략 n log n 번 비교 함수를 호출합니다. 어떤 종류의 알고리즘은 실제로이 비교 함수가 안정적이며 항상 동일한 결과를 반환한다고 기대합니다!
IEnumerableSorter가 예를 들어 quicksort의 각 알고리즘 단계에 대해 비교 함수를 호출하고 매번 x => r.Next()
이 매개 변수를 캐싱하지 않고 두 매개 변수에 대한 함수 를 호출 할 수 는 없습니다!
이 경우 정렬 알고리즘을 엉망으로 만들고 알고리즘이 기대하는 것보다 훨씬 나빠질 수 있습니다. 물론 결국에는 안정되어 무언가를 돌려줍니다.
나중에 새로운 "다음"함수에 디버깅 출력을 넣어서 확인할 수 있으므로 어떻게되는지 확인하십시오. Reflector에서 나는 그것이 어떻게 작동하는지 즉시 알 수 없었습니다.
모든 스레드를 지우고 모든 새로운 테스트를 캐시하여 코드에서 시작하는 시작 시간
첫 번째 실패 코드. LINQPad에서 실행됩니다. 이 코드를 테스트하기 위해 따르는 경우.
Stopwatch st = new Stopwatch();
st.Start();
var r = new Random();
List<string[]> list = new List<string[]>();
list.Add(new String[] {"1","X"});
list.Add(new String[] {"2","A"});
list.Add(new String[] {"3","B"});
list.Add(new String[] {"4","C"});
list.Add(new String[] {"5","D"});
list.Add(new String[] {"6","E"});
//list.OrderBy (l => r.Next()).Dump();
list.OrderBy (l => Guid.NewGuid()).Dump();
st.Stop();
Console.WriteLine(st.Elapsed.TotalMilliseconds);
OrderBy (x => r.Next ())는 38.6528ms를 사용합니다.
list.OrderBy (x => Guid.NewGuid ())는 36.7634ms를 사용합니다 (MSDN에서 권장).
두 번째 이후에는 둘 다 동시에 사용됩니다.
편집 :
Intel Core i7 4@2.1GHz의 테스트 코드, Ram 8GB DDR3 @ 1600, HDD SATA 5200rpm, [데이터 : www.dropbox.com/s/pbtmh5s9lw285kp/data]
using System;
using System.Runtime;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Threading;
namespace Algorithm
{
class Program
{
public static void Main(string[] args)
{
try {
int i = 0;
int limit = 10;
var result = GetTestRandomSort(limit);
foreach (var element in result) {
Console.WriteLine();
Console.WriteLine("time {0}: {1} ms", ++i, element);
}
} catch (Exception e) {
Console.WriteLine(e.Message);
} finally {
Console.Write("Press any key to continue . . . ");
Console.ReadKey(true);
}
}
public static IEnumerable<double> GetTestRandomSort(int limit)
{
for (int i = 0; i < 5; i++) {
string path = null, temp = null;
Stopwatch st = null;
StreamReader sr = null;
int? count = null;
List<string> list = null;
Random r = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Thread.Sleep(5000);
st = Stopwatch.StartNew();
#region Import Input Data
path = Environment.CurrentDirectory + "\\data";
list = new List<string>();
sr = new StreamReader(path);
count = 0;
while (count < limit && (temp = sr.ReadLine()) != null) {
// Console.WriteLine(temp);
list.Add(temp);
count++;
}
sr.Close();
#endregion
// Console.WriteLine("--------------Random--------------");
// #region Sort by Random with OrderBy(random.Next())
// r = new Random();
// list = list.OrderBy(l => r.Next()).ToList();
// #endregion
// #region Sort by Random with OrderBy(Guid)
// list = list.OrderBy(l => Guid.NewGuid()).ToList();
// #endregion
// #region Sort by Random with Parallel and OrderBy(random.Next())
// r = new Random();
// list = list.AsParallel().OrderBy(l => r.Next()).ToList();
// #endregion
// #region Sort by Random with Parallel OrderBy(Guid)
// list = list.AsParallel().OrderBy(l => Guid.NewGuid()).ToList();
// #endregion
// #region Sort by Random with User-Defined Shuffle Method
// r = new Random();
// list = list.Shuffle(r).ToList();
// #endregion
// #region Sort by Random with Parallel User-Defined Shuffle Method
// r = new Random();
// list = list.AsParallel().Shuffle(r).ToList();
// #endregion
// Result
//
st.Stop();
yield return st.Elapsed.TotalMilliseconds;
foreach (var element in list) {
Console.WriteLine(element);
}
}
}
}
}
결과 설명 : https://www.dropbox.com/s/9dw9wl259dfs04g/ResultDescription.PNG
결과 통계 : https://www.dropbox.com/s/ewq5ybtsvesme4d/ResultStat.PNG
결론 :
LINQ OrderBy (r.Next ()) 및 OrderBy (Guid.NewGuid ())는 첫 번째 솔루션의 사용자 정의 셔플 방법보다 나쁘지 않습니다.
답 : 모순입니다.