C #의 List <T>에서 N 개의 임의 원소 선택


158

일반 목록에서 5 개의 임의 요소를 선택하려면 빠른 알고리즘이 필요합니다. 예를 들어,에서 5 개의 임의 요소를 얻고 싶습니다 List<string>.


11
무작위로, 당신은 포함 또는 독점을 의미합니까? 같은 요소를 두 번 이상 선택할 수 있습니까? (진정한 무작위) 또는 요소를 선택하면 더 이상 사용 가능한 풀에서 선택할 수 없습니까?
Pretzel

답변:


127

각 요소에 대해 반복하여 선택 확률 = (필수) / (왼쪽 수)

따라서 40 개의 아이템이 있다면 첫 번째는 5/40의 확률로 선택 될 것입니다. 만약 그렇다면, 다음은 4/39의 기회가 있고, 그렇지 않으면 5/39의 기회가 있습니다. 당신이 마지막에 도착할 때 당신은 당신의 5 개의 품목을 가질 것이고, 종종 당신은 그 전에 모든 것을 가질 것입니다.


33
나는 이것이 미묘하다고 잘못 생각합니다. 백엔드가 훨씬 큰 확률을 볼 수 있으므로 목록의 백엔드가 프런트 엔드보다 더 자주 선택되는 것처럼 보입니다. 예를 들어, 처음 35 개의 숫자를 선택하지 않으면 마지막 5 개의 숫자를 선택해야합니다. 첫 번째 숫자는 5/40 확률 만 볼 수 있지만 마지막 숫자는 1/15보다 5/40 배 더 자주 나타납니다. 이 알고리즘을 구현하기 전에 먼저 목록을 무작위 화해야합니다.
Ankur Goel

23
좋아, 나는이 알고리즘을 40 개 요소 목록에서 1 천만 번 실행했으며, 각 요소는 선택시 5/40 (.125) 샷으로 여러 번 실행했습니다. 이것이 균등하게 분배되지 않은 것으로 나타났습니다. 16에서 22까지의 요소는 선택이 취소되고 (16 = .123, 17 = .124), 요소 34는 선택이 초과됩니다 (34 = .129). 요소 39와 40도 선택되지 않았지만 많이 선택되지는 않았습니다 (39 = .1247, 40 = .1246)
Ankur Goel

21
@ Ankur : 나는 그것이 통계적으로 중요하다고 생각하지 않습니다. 나는 이것이 균일 한 분포를 제공 할 것이라는 귀납적 인 증거가 있다고 믿는다.
재귀

9
동일한 평가판을 1 억 번 반복했으며, 평가판에서 가장 적게 선택한 항목이 가장 자주 선택한 항목보다 0.106 % 미만 덜 선택되었습니다.
재귀

5
@ 재귀 : 증거는 거의 사소합니다. 우리는 K에 대해 K에서 K 항목을 선택하는 방법과 N에 대해 N에서 0 항목을 선택하는 방법을 알고 있습니다. N-1> = K에서 K 또는 K-1 항목을 균일하게 선택하는 방법을 알고 있다고 가정합니다. 그런 다음 확률 K / N 인 첫 번째 항목을 선택한 다음 알려진 방법을 사용하여 나머지 N-1에서 여전히 필요한 K 또는 K-1 항목을 선택하여 N에서 K 항목을 선택할 수 있습니다.
Ilmari Karonen

216

linq 사용하기 :

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

2
+1 그러나 두 요소가 rnd.Next () 또는 이와 유사한 숫자를 가져 오면 첫 번째 요소가 선택되고 두 번째 요소는 더 이상 요소가 더 이상 필요하지 않은 경우 선택됩니다. 그러나 사용법에 따라 적절하게 무작위입니다.
Lasse Espeholt

7
순서는 O (n log (n))라고 생각하므로 코드 단순성이 주요 관심사 인 경우 (예 : 작은 목록)이 솔루션을 선택합니다.
Guido

2
그러나 이것이 전체 목록을 열거하고 정렬하지 않습니까? "빠른"이 아니라면 OP는 "성능적인"이 아니라 "쉬운"을 의미했습니다 ...
drzaus

2
OrderBy ()가 각 요소에 대해 키 선택기를 한 번만 호출하는 경우에만 작동합니다. 두 요소 사이의 비교를 수행하려고 할 때마다 호출하면 매번 다른 값을 다시 가져 와서 정렬을 망칠 것입니다. [documentation] ( msdn.microsoft.com/en-us/library/vstudio/… )은 어떤 내용을 말하지 않습니다.
Oliver Bock

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) Kyle의 대답은 O (n)입니다.

(1) n 쌍 [(0, rand), (1, rand), (2, rand), ...]의 목록을 생성하고 두 번째 좌표로 정렬하고 첫 번째 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의 경우 더 빠를 수 있습니다. 정렬을 유지하고 인접한 간격을 추적하면 O (k log k)로 구현 할 수 있지만 더 많은 작업입니다.

@Kyle-네 말이 맞아, 나는 네 대답에 동의한다고 생각했다. 나는 처음에 급히 그것을 읽었고, 실수로 고정 확률 k / n을 가진 각 요소를 순차적으로 선택한다고 잘못 생각하고 있었지만 잘못 생각했을 것입니다. 미안합니다.

자, 이제 키커 : 무증상 (고정 k, n 증가)에는 n ^ k / k가 있습니다! n 개의 요소 중 k 개의 요소 서브 세트의 선택 [이것은 근사치 (n choose k)]입니다. n이 크고 k가 그다지 작지 않으면이 숫자는 엄청납니다. 표준 32 비트 난수 생성기에서 기대할 수있는 최상의 사이클 길이는 2 ^ 32 = 256 ^ 4입니다. 따라서 1000 개의 요소 목록이 있고 무작위로 5를 선택하려는 경우 표준 난수 생성기가 모든 가능성을 달성 할 방법이 없습니다. 그러나 더 작은 세트에 대해 잘 작동하고 항상 "임의로"보이는 선택에 만족하는 경우 이러한 알고리즘은 정상입니다.

부록 :이 글을 쓴 후에는 아이디어 (2)를 올바르게 구현하는 것이 까다 롭다는 것을 깨달았 으므로이 대답을 분명히하고 싶었습니다. O (k log k) 시간을 얻으려면 O (log m) 검색 및 삽입을 지원하는 배열과 유사한 구조가 필요합니다. 균형 잡힌 이진 트리가이를 수행 할 수 있습니다. 이러한 구조를 사용하여 s라는 배열을 작성하면 의사 파이썬이 있습니다.

# 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의 사이클 길이에 대해 이의를 제기 한 경우 동일한 확률로 모든 세트를 선택할 알고리즘을 구성 할 있는 방법이 있습니까?
Jonah

(1)의 경우 O (n log (n))을 개선하기 위해 선택 정렬을 사용하여 k 개의 가장 작은 요소를 찾을 수 있습니다. O (n * k)로 실행됩니다.
Jared

@Jonah : 그렇게 생각합니다. 여러 개의 독립적 난수 생성기를 결합하여 더 큰 것을 생성한다고 가정 해 봅시다 ( 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

아니요, 기본값이 존재한다는 사실을 알지 못했습니다.
Frank Schwieterman

randomSortTable의 증명 : randomSortTable = someTypes.ToDictionary (x => random.NextDouble (), y => y); foreach 루프를 저장합니다.
Keltex

2
일년에 늦었지만 OK. @ersin의 짧은 대답으로 넘어 가지 않고 반복되는 난수를 얻더라도 실패하지 않습니다 (Ersin의 반복 쌍의 첫 번째 항목에 대한 편향이있는 곳)
Andiih

12

방금이 문제에 부딪 쳤고 더 많은 Google 검색으로 인해 목록을 무작위로 섞는 문제가 발생했습니다. 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) 실행 시간

마지막으로, 실제 stickler의 경우 (n == k) 인 경우 무작위로 선택한 정수가 항상 0이므로 nk가 아닌 1에서 정지해야합니다.


내 블로그 게시물 vijayt.com/post/random-select-using-fisher-yates-algorithm 에서 C #을 사용하여 구현했습니다 . 누군가가 C # 방식을 찾는 데 도움이되기를 바랍니다.
vijayst


8

에서 알고리즘의 드래곤 , 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

2
var결과를 사용 needed하고 available정수를 사용하므로 needed/available항상 0이
Niko

1
이것은 허용 된 답변의 구현으로 보입니다.
DCShannon

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) 원래 그룹의 복제본을 고려하여 그룹과 완전히 다른 임의의 값이 필요한 경우 위와 동일한 방법을 사용할 수 있지만 a 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);
}

목록에있는 항목의 1000 수십와의 모든에 대한 성능은 10000 번 반복 할 필요가 있다면, 당신은 할 수 있습니다 빠른 임의의 클래스 이상을 System.Random,하지만 난 그게 가장 아마 후자를 고려 큰 문제가 결코 아니다 생각하지 않는다 병목 현상, 충분히 빠릅니다 ..

편집 : 반환 된 항목의 순서를 다시 정렬 해야하는 경우 dhakim의 Fisher-Yates 접근법을 능가 할 수있는 것은 없습니다 -짧고 달콤하고 단순합니다.


6

@JohnShedletsky의 의견에 대해 (paraphrase) 에 대해 허용 된 답변 에 대해 생각하고있었습니다 .

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 2016 년

1
스 래싱 문제에 대한 나의 해결책은 이것입니다 : 만약 K <N / 2라면, 당신의 방식으로하십시오. K> = N / 2 인 경우 유지해야하는 인덱스 대신 유지해서는 안되는 인덱스를 선택하십시오. 여전히 일부 스레 싱이 있지만 훨씬 적습니다.
Paul Chernoch 2016 년

또한 이는 열거되는 항목의 순서를 변경하여 일부 상황에서는 허용되지만 다른 상황에서는 허용되지 않을 수도 있습니다.
Paul Chernoch 2016 년

평균적으로 K = N / 2 (Paul의 제안 된 개선에 대한 최악의 경우)의 경우 (스 래싱 개선) 알고리즘은 ~ 0.693 * N 반복을 수행하는 것으로 보입니다. 이제 속도를 비교하십시오. 이것이 대답보다 낫습니까? 어떤 표본 크기에 적합합니까?
mbomb007

6

위의 답변 중 몇 가지를 결합하여 지연 평가 확장 방법을 만들었습니다. 내 테스트에 따르면 Kyle의 접근 방식 (Order (N))은 drzaus가 무작위 지수를 선택하도록 제안하는 세트를 사용하는 것보다 몇 배 느립니다 (Order (K)). 전자는 난수 생성기에 대해 더 많은 호출을 수행하고 항목에 대해 더 많은 시간을 반복합니다.

구현의 목표는 다음과 같습니다.

1) IList가 아닌 IEnumerable이 제공된 경우 전체 목록을 인식하지 마십시오. 일련의 zillion 항목이 주어지면 메모리가 부족하지 않습니다. 온라인 솔루션에는 Kyle의 접근 방식을 사용하십시오.

2) 그것이 IList라고 말할 수 있다면 drzaus의 접근 방식을 사용하십시오. K가 N의 절반 이상이면 랜덤 인덱스를 여러 번 반복해서 선택하고 건너 뛰면 스 래싱 위험이 있습니다. 따라서 나는 유지하지 않기 위해 지수 목록을 작성합니다.

3) 상품이 발생한 순서대로 반환됩니다. Kyle의 알고리즘은 변경하지 않아도됩니다. drzaus의 알고리즘은 임의의 인덱스가 선택된 순서대로 항목을 방출하지 않아야했습니다. 모든 인덱스를 SortedSet에 모은 다음 정렬 된 인덱스 순서로 항목을 내 보냅니다.

4) K가 N에 비해 크고 세트의 의미를 뒤집 으면 모든 항목을 열거하고 인덱스가 세트에 없는지 테스트합니다. 이것은 주문 (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는 콜린 그린에 의해 작성 및 SharpNEAT의 일부가되었습니다. 그것은 많은 RNG에보다 더 2 ^ 128-1의 기간이 있습니다.)

다음은 단위 테스트입니다.

[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
첫 촬영에서 일했다!
sangam

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 2016 년

3

여기에는 John Shedletsky가 지적한 것처럼 알고리즘 복잡도가 O (n) 인 Fisher-Yates Shuffle을 기반으로 한 구현이 있습니다. 여기서 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

Kyle의 답변을 바탕으로 내 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

이 방법은 Kyle의 방법과 동일 할 수 있습니다.

목록의 크기가 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
그것은 나에게 동등한 것으로 보인다.
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 목록에로드 된 다음 List의 정렬 기능 과 선택된 첫 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 요소를 선택하십시오. 파이썬에서는

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
Complexity : 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

흠. 진실. 내가 그것을 사용하는 곳은 중요하지 않습니다. 그것을 반영하기 위해 답변을 편집했습니다.
Wolf5

-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.