Chọn N phần tử ngẫu nhiên từ Danh sách <T> trong C #


158

Tôi cần một thuật toán nhanh để chọn 5 yếu tố ngẫu nhiên từ một danh sách chung. Ví dụ: tôi muốn nhận 5 yếu tố ngẫu nhiên từ a List<string>.


11
Ngẫu nhiên, bạn có nghĩa là bao gồm hoặc độc quyền? IOW, yếu tố tương tự có thể được chọn nhiều lần không? (thực sự ngẫu nhiên) Hoặc một khi một yếu tố được chọn, nó sẽ không còn có thể được chọn từ nhóm có sẵn?
Pretzel

Câu trả lời:


127

Lặp lại qua và cho mỗi phần tử làm cho xác suất lựa chọn = (số cần thiết) / (số còn lại)

Vì vậy, nếu bạn có 40 vật phẩm, đầu tiên sẽ có 5/40 cơ hội được chọn. Nếu có, tiếp theo có cơ hội 4/39, nếu không, nó có cơ hội 5/39. Khi bạn kết thúc, bạn sẽ có 5 món đồ của mình và thường thì bạn sẽ có tất cả chúng trước đó.


33
Tôi cảm thấy điều này là sai tinh tế. Có vẻ như mặt sau của danh sách sẽ được chọn thường xuyên hơn mặt trước vì mặt sau sẽ có xác suất lớn hơn nhiều. Ví dụ: nếu 35 số đầu tiên không được chọn, 5 số cuối phải được chọn. Số đầu tiên sẽ chỉ nhìn thấy cơ hội 5/40, nhưng số cuối cùng đó sẽ thấy 1/1 thường xuyên hơn 5/40 lần. Bạn sẽ phải chọn ngẫu nhiên danh sách trước khi bạn thực hiện thuật toán này.
Ankur Goel

23
ok, tôi đã chạy thuật toán này 10 triệu lần trong danh sách 40 phần tử, mỗi phần tử có 5/40 (.125) khi được chọn, và sau đó chạy mô phỏng đó nhiều lần. Nó chỉ ra rằng điều này không được phân phối đều. Các phần tử 16 đến 22 được chọn không được chọn (16 = .123, 17 = .124), trong khi phần tử 34 được chọn quá mức (34 = .129). Các yếu tố 39 và 40 cũng được chọn không được chọn nhưng không nhiều (39 = .1247, 40 = .1246)
Ankur Goel

21
@Ankur: Tôi không tin điều đó có ý nghĩa thống kê. Tôi tin rằng có một bằng chứng quy nạp rằng điều này sẽ cung cấp một phân phối đồng đều.
đệ quy

9
Tôi đã lặp lại cùng một thử nghiệm 100 triệu lần và trong thử nghiệm của tôi, mặt hàng được chọn ít nhất được chọn ít hơn 0,19% so với mặt hàng được chọn thường xuyên nhất.
đệ quy

5
@recursive: Bằng chứng gần như tầm thường. Chúng tôi biết cách chọn K mục trong số K cho bất kỳ K nào và cách chọn 0 mục trong số N cho bất kỳ N. Giả sử chúng tôi biết một phương pháp để chọn thống nhất các mục K hoặc K-1 trong số N-1> = K; sau đó chúng ta có thể chọn K mục trong số N bằng cách chọn mục đầu tiên có xác suất K / N và sau đó sử dụng phương thức đã biết để chọn các mục K hoặc K-1 vẫn cần thiết trong số N-1 còn lại.
Ilmari Karonen

216

Sử dụng linq:

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

2
+1 Nhưng nếu hai phần tử có cùng số từ rnd.Next () hoặc tương tự thì phần tử thứ nhất sẽ được chọn và phần tử thứ hai có thể sẽ không (nếu không cần thêm phần tử nào). Nó là đủ ngẫu nhiên đủ tùy thuộc vào việc sử dụng, mặc dù.
Lasse Espeholt

7
Tôi nghĩ thứ tự của là O (n log (n)), vì vậy tôi sẽ chọn giải pháp này nếu tính đơn giản của mã là mối quan tâm chính (tức là với các danh sách nhỏ).
Guido

2
Nhưng điều này không liệt kê và sắp xếp toàn bộ danh sách? Trừ khi, bằng cách "nhanh chóng", OP có nghĩa là "dễ dàng", không phải "biểu diễn" ...
drzaus

2
Điều này sẽ chỉ hoạt động nếu OrderBy () chỉ gọi bộ chọn khóa một lần cho mỗi phần tử. Nếu nó gọi nó bất cứ khi nào nó muốn thực hiện so sánh giữa hai phần tử thì nó sẽ nhận được một giá trị khác nhau mỗi lần, điều này sẽ làm hỏng việc sắp xếp. [Tài liệu] ( msdn.microsoft.com/en-us/l Library / vudio / ' ) không cho biết đó là gì.
Oliver Bock

2
Xem ra nếu YourListcó nhiều mặt hàng nhưng bạn chỉ muốn chọn một vài mặt hàng. Trong trường hợp này, nó không phải là một cách hiệu quả để làm điều đó.
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

Đây thực sự là một vấn đề khó hơn so với nó, chủ yếu là do nhiều giải pháp đúng về mặt toán học sẽ không thực sự cho phép bạn đạt được tất cả các khả năng (nhiều hơn về điều này dưới đây).

Đầu tiên, đây là một số trình tạo số dễ thực hiện, chính xác nếu bạn có một số thực sự ngẫu nhiên:

(0) Câu trả lời của Kyle, đó là O (n).

(1) Tạo danh sách n cặp [(0, rand), (1, rand), (2, rand), ...], sắp xếp chúng theo tọa độ thứ hai và sử dụng k đầu tiên (cho bạn, k = 5) chỉ số để có tập hợp con ngẫu nhiên của bạn. Tôi nghĩ rằng điều này là dễ thực hiện, mặc dù đó là thời gian O (n log n).

(2) Ban đầu một danh sách trống s = [] sẽ phát triển thành các chỉ số của k phần tử ngẫu nhiên. Chọn một số r trong {0, 1, 2, ..., n-1} một cách ngẫu nhiên, r = rand% n và thêm số này vào s. Tiếp theo lấy r = rand% (n-1) và dán vào s; thêm vào r các phần tử # nhỏ hơn nó trong s để tránh va chạm. Tiếp theo lấy r = rand% (n-2) và làm điều tương tự, v.v. cho đến khi bạn có k phần tử riêng biệt trong s. Điều này có thời gian chạy trường hợp xấu nhất O (k ^ 2). Vì vậy, đối với k << n, điều này có thể nhanh hơn. Nếu bạn tiếp tục sắp xếp và theo dõi các khoảng tiếp giáp nhau, bạn có thể triển khai nó trong O (k log k), nhưng công việc sẽ hiệu quả hơn.

@Kyle - bạn nói đúng, về ý nghĩ thứ hai tôi đồng ý với câu trả lời của bạn. Lúc đầu, tôi vội vàng đọc nó và nghĩ rằng bạn đang chỉ định chọn tuần tự từng yếu tố với xác suất cố định k / n, điều này sẽ sai - nhưng cách tiếp cận thích ứng của bạn có vẻ đúng với tôi. Xin lỗi vì điều đó.

Ok, và bây giờ cho kicker: tiệm cận (đối với k cố định, n đang phát triển), có n ^ k / k! các lựa chọn của tập hợp phần tử k trong số n phần tử [đây là một xấp xỉ của (n chọn k)]. Nếu n lớn và k không nhỏ lắm thì những con số này rất lớn. Độ dài chu kỳ tốt nhất bạn có thể hy vọng trong bất kỳ trình tạo số ngẫu nhiên 32 bit tiêu chuẩn nào là 2 ^ 32 = 256 ^ 4. Vì vậy, nếu chúng ta có một danh sách 1000 phần tử và chúng ta muốn chọn 5 phần tử một cách ngẫu nhiên, không có cách nào một trình tạo số ngẫu nhiên tiêu chuẩn sẽ đạt được tất cả các khả năng. Tuy nhiên, miễn là bạn ổn với lựa chọn hoạt động tốt cho các tập nhỏ hơn và luôn "trông" ngẫu nhiên, thì các thuật toán này sẽ ổn.

Phụ lục : Sau khi viết bài này, tôi nhận ra rằng thật khó để thực hiện ý tưởng (2) một cách chính xác, vì vậy tôi muốn làm rõ câu trả lời này. Để có thời gian O (k log k), bạn cần một cấu trúc giống như mảng hỗ trợ tìm kiếm và chèn O (log m) - một cây nhị phân cân bằng có thể làm điều này. Sử dụng cấu trúc như vậy để xây dựng một mảng gọi là s, đây là một số giả:

# 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

Tôi đề nghị chạy qua một vài trường hợp mẫu để xem cách này thực hiện hiệu quả lời giải thích bằng tiếng Anh ở trên.


2
đối với (1) bạn có thể xáo trộn danh sách nhanh hơn so với sắp xếp, đối với (2) bạn sẽ thiên vị phân phối của mình bằng cách sử dụng%
jk.

Với sự phản đối mà bạn nêu ra về độ dài chu kỳ của một rng, có cách nào chúng ta có thể xây dựng một thuật toán sẽ chọn tất cả các tập hợp với xác suất bằng nhau không?
Giô-na

Đối với (1), để cải thiện O (n log (n)), bạn có thể sử dụng sắp xếp lựa chọn để tìm k phần tử nhỏ nhất. Điều đó sẽ chạy trong O (n * k).
Jared

@Jonah: Tôi nghĩ vậy. Giả sử chúng ta có thể kết hợp nhiều trình tạo số ngẫu nhiên độc lập để tạo ra một trình tạo số lớn hơn ( crypto.stackexchange.com/a/27431 ). Sau đó, bạn chỉ cần một phạm vi đủ lớn để đối phó với kích thước của danh sách được đề cập.
Jared

16

Tôi nghĩ rằng câu trả lời được chọn là chính xác và khá ngọt ngào. Tôi đã thực hiện nó theo cách khác, vì tôi cũng muốn kết quả theo thứ tự ngẫu nhiên.

    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);
    }

TUYỆT VỜI! Thực sự đã giúp tôi ra ngoài!
Armstrongest

1
Bạn có bất kỳ lý do nào để không sử dụng Random () mới dựa trên Môi trường.TickCount so với DateTime.Now.Millisecond không?
Lasse Espeholt

Không, chỉ là không biết rằng tồn tại mặc định.
Frank Schwieterman

Một sự tham gia của RandomSortTable: RandomSortTable = someTypes.ToDipedia (x => Random.NextDouble (), y => y); Lưu vòng lặp foreach.
Keltex

2
OK một năm muộn nhưng ... Điều này không phù hợp với câu trả lời ngắn hơn của @ ersin và sẽ không thất bại nếu bạn nhận được một số ngẫu nhiên lặp lại (Trường hợp Ersin sẽ có xu hướng đối với mục đầu tiên của một cặp lặp lại)
Andiih

12

Tôi vừa gặp phải vấn đề này và một số tìm kiếm khác của google đã đưa tôi đến vấn đề xáo trộn ngẫu nhiên một danh sách: http://en.wikipedia.org/wiki/Fisher-Yates_shuffle

Để ngẫu nhiên xáo trộn danh sách của bạn (tại chỗ), bạn làm điều này:

Để xáo trộn một mảng a gồm n phần tử (chỉ số 0..n-1):

  for i from n  1 downto 1 do
       j  random integer with 0  j  i
       exchange a[j] and a[i]

Nếu bạn chỉ cần 5 phần tử đầu tiên, thì thay vì chạy i suốt từ n-1 đến 1, bạn chỉ cần chạy nó đến n-5 (tức là: n-5)

Hãy nói rằng bạn cần k mục,

Điều này trở thành:

  for (i = n  1; i >= n-k; i--)
  {
       j = random integer with 0  j  i
       exchange a[j] and a[i]
  }

Mỗi mục được chọn được hoán đổi ở cuối mảng, vì vậy các phần tử k được chọn là phần tử k cuối cùng của mảng.

Điều này làm mất thời gian O (k), trong đó k là số phần tử được chọn ngẫu nhiên bạn cần.

Hơn nữa, nếu bạn không muốn sửa đổi danh sách ban đầu của mình, bạn có thể ghi lại tất cả các giao dịch hoán đổi của mình trong một danh sách tạm thời, đảo ngược danh sách đó và áp dụng lại chúng, do đó thực hiện bộ hoán đổi nghịch đảo và trả về danh sách ban đầu của bạn mà không thay đổi thời gian chạy O (k).

Cuối cùng, đối với stickler thực, nếu (n == k), bạn nên dừng ở 1, không phải nk, vì số nguyên được chọn ngẫu nhiên sẽ luôn là 0.


Tôi đã triển khai nó bằng C # trong bài đăng trên blog của mình: vijayt.com/post/random-select-USE-fisher-yates-alacticm . Hy vọng nó sẽ giúp ai đó tìm kiếm cách C #.
vijayst


8

Từ những con rồng trong thuật toán , một cách giải thích trong 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--;
}

Thuật toán này sẽ chọn các chỉ số duy nhất của danh sách các mục.


Chỉ nhận đủ mục trong danh sách, nhưng không nhận được ngẫu nhiên.
vào

2
Việc triển khai này bị hỏng vì sử dụng varkết quả trong neededavailablecả hai là số nguyên, needed/availableluôn luôn là 0.
Niko

1
Điều này dường như là một thực hiện của câu trả lời được chấp nhận.
DCShannon

6

Chọn N mục ngẫu nhiên từ một nhóm không nên có bất cứ điều gì liên quan đến đơn hàng ! Sự ngẫu nhiên là về sự khó lường và không phải là về các vị trí xáo trộn trong một nhóm. Tất cả các câu trả lời liên quan đến một số loại đặt hàng chắc chắn là kém hiệu quả hơn so với những câu trả lời không. Vì hiệu quả là chìa khóa ở đây, tôi sẽ đăng một cái gì đó không thay đổi thứ tự của các mặt hàng quá nhiều.

1) Nếu bạn cần các giá trị ngẫu nhiên thực sự , điều đó có nghĩa là không có hạn chế nào về việc lựa chọn các yếu tố nào (nghĩa là, một khi mục được chọn có thể được chọn lại):

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;
}

Nếu bạn đặt cờ ngoại lệ, thì bạn có thể chọn các mục ngẫu nhiên bất kỳ số lần.

Nếu bạn có {1, 2, 3, 4}, thì nó có thể cung cấp {1, 4, 4}, {1, 4, 3} vv cho 3 mục hoặc thậm chí {1, 4, 3, 2, 4} cho 5 món!

Điều này sẽ khá nhanh, vì nó không có gì để kiểm tra.

2) Nếu bạn cần các thành viên riêng lẻ trong nhóm mà không lặp lại, thì tôi sẽ dựa vào một từ điển (như nhiều người đã chỉ ra).

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();
}

Mã này dài hơn một chút so với các cách tiếp cận từ điển khác ở đây vì tôi không chỉ thêm, mà còn xóa khỏi danh sách, do đó, đây là hai vòng lặp. Bạn có thể thấy ở đây tôi chưa sắp xếp lại bất cứ thứ gì khi counttrở nên ngang bằng source.Count. Đó là bởi vì tôi tin rằng ngẫu nhiên phải ở trong bộ trở lại như một toàn thể . Ý tôi là nếu bạn muốn 5 mục ngẫu nhiên từ 1, 2, 3, 4, 5đó, thì không quan trọng nếu nó 1, 3, 4, 2, 5hay 1, 2, 3, 4, 5, nhưng nếu bạn cần 4 mục từ cùng một tập hợp, sau đó nó không thể lường trước nên năng suất trong 1, 2, 3, 4, 1, 3, 5, 2, 2, 3, 5, 4, vv Thứ hai, khi số lượng các mặt hàng ngẫu nhiên để được trả lại là hơn một nửa của nhóm ban đầu, sau đó dễ dàng hơn để loại bỏsource.Count - countcác mục từ nhóm hơn là thêm countcác mục. Vì lý do hiệu suất, tôi đã sử dụng sourcethay vì sourceDictlấy chỉ số ngẫu nhiên trong phương thức remove.

Vì vậy, nếu bạn có {1, 2, 3, 4}, điều này có thể kết thúc bằng {1, 2, 3}, {3, 4, 1}, v.v. cho 3 mục.

3) Nếu bạn cần các giá trị ngẫu nhiên thực sự khác biệt với nhóm của mình bằng cách tính đến các bản sao trong nhóm ban đầu, thì bạn có thể sử dụng cùng một cách tiếp cận như trên, nhưng HashSetsẽ nhẹ hơn từ điển.

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();
}

Các randomsbiến được làm một HashSetđể tránh trùng lặp được thêm vào trong hiếm các trường hợp hiếm nhất nơi Random.Nextcó thể mang lại giá trị như nhau, đặc biệt là khi danh sách đầu vào là nhỏ.

Vậy {1, 2, 2, 4} => 3 mục ngẫu nhiên => {1, 2, 4} và không bao giờ {1, 2, 2}

{1, 2, 2, 4} => 4 mục ngẫu nhiên => ngoại lệ !! hoặc {1, 2, 4} tùy thuộc vào bộ cờ.

Một số phương pháp mở rộng tôi đã sử dụng:

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);
}

Nếu tất cả chỉ là về hiệu suất với hàng chục 1000 mặt hàng trong danh sách phải lặp đi lặp lại 10000 lần, thì bạn có thể muốn có lớp ngẫu nhiên nhanh hơn System.Random, nhưng tôi không nghĩ rằng đó là vấn đề lớn có lẽ không bao giờ là một nút cổ chai, nó khá đủ nhanh ..

Chỉnh sửa: Nếu bạn cũng cần sắp xếp lại thứ tự các mặt hàng được trả lại, thì không có gì có thể vượt qua cách tiếp cận Fisher-Yates của dhakim - ngắn gọn, ngọt ngào và đơn giản ..


6

Đã suy nghĩ về nhận xét của @JohnShedletsky về câu trả lời được chấp nhận liên quan đến ( paraphotto ):

bạn sẽ có thể làm điều này trong O (tập hợp con. Độ dài), thay vì O (gốcList.Lipse)

Về cơ bản, bạn sẽ có thể tạo subsetcác chỉ số ngẫu nhiên và sau đó nhổ chúng từ danh sách ban đầu.

Phương pháp

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()));
    }

}

Nếu bạn muốn hiệu quả hơn nữa, có lẽ bạn sẽ sử dụng một HashSettrong các chỉ số , không phải các yếu tố danh sách thực tế (trong trường hợp bạn có các loại phức tạp hoặc so sánh đắt tiền);

Bài kiểm tra đơn vị

Và để chắc chắn rằng chúng ta không có bất kỳ va chạm nào, v.v.

[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
Ý tưởng hay, có vấn đề. (1) Nếu danh sách lớn hơn của bạn rất lớn (ví dụ đọc từ cơ sở dữ liệu) thì bạn nhận ra toàn bộ danh sách, có thể vượt quá bộ nhớ. (2) Nếu K gần với N, thì bạn sẽ tìm kiếm rất nhiều chỉ số không được yêu cầu trong vòng lặp của mình, khiến mã yêu cầu một lượng thời gian không thể đoán trước. Những vấn đề này có thể giải quyết được.
Paul Chernoch

1
Giải pháp của tôi cho vấn đề đập là thế này: nếu K <N / 2, hãy làm theo cách của bạn. Nếu K> = N / 2, hãy chọn các chỉ số KHÔNG nên giữ, thay vì các chỉ số cần giữ. Vẫn còn một số đập, nhưng ít hơn nhiều.
Paul Chernoch

Cũng lưu ý rằng điều này làm thay đổi thứ tự của các mục được liệt kê, có thể được chấp nhận trong một số trường hợp, nhưng không phải trong các tình huống khác.
Paul Chernoch

Trung bình, đối với K = N / 2 (trường hợp xấu nhất đối với cải tiến được đề xuất của Paul), thuật toán (cải thiện đập) dường như mất ~ 0,693 * N lần lặp. Bây giờ làm một so sánh tốc độ. Điều này có tốt hơn câu trả lời được chấp nhận không? Đối với cỡ mẫu nào?
mbomb007

6

Tôi đã kết hợp một số câu trả lời ở trên để tạo phương thức mở rộng được đánh giá theo Lazily. Thử nghiệm của tôi cho thấy cách tiếp cận của Kyle (Đơn hàng (N)) chậm hơn nhiều lần so với việc sử dụng một bộ của drzaus để đề xuất các chỉ số ngẫu nhiên để chọn (Đơn hàng (K)). Cái trước thực hiện nhiều cuộc gọi hơn đến trình tạo số ngẫu nhiên, cộng với số lần lặp nhiều lần hơn các mục.

Mục tiêu thực hiện của tôi là:

1) Không nhận ra danh sách đầy đủ nếu được cung cấp một IEnumerable không phải là IList. Nếu tôi được cung cấp một chuỗi gồm hàng triệu vật phẩm, tôi không muốn hết bộ nhớ. Sử dụng phương pháp của Kyle cho một giải pháp trực tuyến.

2) Nếu tôi có thể nói rằng đó là IList, hãy sử dụng phương pháp của drzaus, với một bước ngoặt. Nếu K là hơn một nửa N, tôi có nguy cơ bị đập khi tôi chọn nhiều chỉ số ngẫu nhiên hết lần này đến lần khác và phải bỏ qua chúng. Vì vậy, tôi soạn một danh sách các chỉ số để KHÔNG giữ.

3) Tôi đảm bảo rằng các mặt hàng sẽ được trả lại theo cùng thứ tự mà chúng đã gặp phải. Thuật toán của Kyle không yêu cầu thay đổi. Thuật toán của drzaus yêu cầu tôi không phát ra các mục theo thứ tự các chỉ số ngẫu nhiên được chọn. Tôi tập hợp tất cả các chỉ mục vào một Sắp xếp, sau đó phát ra các mục theo thứ tự chỉ mục được sắp xếp.

4) Nếu K lớn so với N và tôi đảo ngược ý nghĩa của tập hợp, thì tôi liệt kê tất cả các mục và kiểm tra nếu chỉ mục không có trong tập hợp. Điều này có nghĩa là tôi mất thời gian chạy Đơn hàng (K), nhưng vì K gần với N trong những trường hợp này, tôi không mất nhiều.

Đây là mã:

    /// <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;
                }
            }
        }  
    } 

Tôi sử dụng trình tạo số ngẫu nhiên chuyên dụng, nhưng bạn chỉ có thể sử dụng Ngẫu nhiên của C # nếu muốn. ( FastRandom được viết bởi Colin Green và là một phần của SharpNEAT. Nó có thời gian 2 ^ 128-1, tốt hơn nhiều RNG.)

Dưới đây là các bài kiểm tra đơn vị:

[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));
    }
}

Không có lỗi trong bài kiểm tra? Bạn có if (itemsList != null && k < n/2)nghĩa là bên trong if invertSetluôn luôn falsecó nghĩa là logic không bao giờ được sử dụng.
NetMage

4

Mở rộng từ câu trả lời của @ ers, nếu ai đó lo lắng về việc triển khai OrderBy khác nhau, điều này sẽ an toàn:

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

Đây là điều tốt nhất tôi có thể đưa ra trong lần cắt đầu tiên:

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;
}

Sử dụng danh sách các randoms trong phạm vi 1 - tổng số danh sách và sau đó chỉ cần kéo các mục đó trong danh sách là cách tốt nhất, nhưng sử dụng Từ điển để đảm bảo tính duy nhất là điều tôi vẫn còn nhầm lẫn.

Cũng lưu ý tôi đã sử dụng một danh sách chuỗi, thay thế khi cần thiết.


1
Làm việc ở phát bắn đầu tiên!
sangam

3

Giải pháp đơn giản tôi sử dụng (có thể không tốt cho danh sách lớn): Sao chép danh sách vào danh sách tạm thời, sau đó trong vòng lặp chọn ngẫu nhiên Mục từ danh sách tạm thời và đưa nó vào danh sách các mục đã chọn trong khi xóa danh sách tạm thời (vì vậy không thể chọn lại).

Thí dụ:

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++;
 }

Loại bỏ từ giữa danh sách thường sẽ rất tốn kém. Bạn có thể xem xét sử dụng danh sách được liên kết cho một thuật toán yêu cầu rất nhiều lần xóa. Hoặc tương tự, thay thế mục bị xóa bằng một giá trị null, nhưng sau đó bạn sẽ phá hủy một chút khi bạn chọn các mục đã bị xóa và phải chọn lại.
Paul Chernoch

3

Ở đây bạn có một triển khai dựa trên Fisher-Yates Shuffle có độ phức tạp thuật toán là O (n) trong đó n là tập hợp con hoặc cỡ mẫu, thay vì kích thước danh sách, như John Shedletsky đã chỉ ra.

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

Dựa trên câu trả lời của Kyle, đây là cách thực hiện c # của tôi.

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

Phương pháp này có thể tương đương với Kyle.

Nói rằng danh sách của bạn có kích thước n và bạn muốn k phần tử.

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--;
    }
} 

Hoạt động như một bùa mê :)

-Alex Gilbert


1
Điều đó có vẻ tương đương với tôi. So sánh với stackoverflow
tự.com/a/48141/2449863

1

Tại sao không phải là một cái gì đó như thế này:

 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

Mục tiêu: Chọn N số mục từ nguồn bộ sưu tập mà không trùng lặp. Tôi đã tạo một phần mở rộng cho bất kỳ bộ sưu tập chung chung. Đây là cách tôi đã làm:

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

Gần đây tôi đã làm điều này trong dự án của mình bằng một ý tưởng tương tự như điểm 1 của Tyler .
Tôi đã tải một loạt các câu hỏi và chọn năm câu hỏi ngẫu nhiên. Sắp xếp đã đạt được bằng cách sử dụng một IComparer .
a Tất cả các câu hỏi đã được tải trong danh sách Câu hỏi, sau đó được sắp xếp bằng hàm Sắp xếp của Danh sách và các phần tử k đầu tiên được chọn.

    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;
            }
        }
    }

Sử dụng:

    List<QuestionSorter> unsortedQuestions = new List<QuestionSorter>();

    // add the questions here

    unsortedQuestions.Sort(unsortedQuestions as IComparer<QuestionSorter>);

    // select the first k elements

0

Đây là cách tiếp cận của tôi (toàn văn ở đây http://krkadev.blogspot.com/2010/08/random-numbers-without-repetition.html ).

Nó nên chạy trong O (K) thay vì O (N), trong đó K là số phần tử mong muốn và N là kích thước của danh sách để chọ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

Đây không phải là thanh lịch hoặc hiệu quả như giải pháp được chấp nhận, nhưng nó nhanh chóng để viết lên. Đầu tiên, hoán vị mảng ngẫu nhiên, sau đó chọn các phần tử K đầu tiên. Trong trăn

import numpy

N = 20
K = 5

idx = np.arange(N)
numpy.random.shuffle(idx)

print idx[:K]

0

Tôi sẽ sử dụng một phương pháp mở rộng.

    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;
        }
    }

Bộ nhớ: ~ tính Độ
phức tạp: O (đếm 2 )


0

Khi N rất lớn, phương thức bình thường xáo trộn ngẫu nhiên các số N và chọn, giả sử, số k đầu tiên, có thể bị cấm vì độ phức tạp của không gian. Thuật toán sau chỉ yêu cầu O (k) cho cả độ phức tạp thời gian và không gian.

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

Sử dụng LINQ với danh sách lớn (khi tốn kém để chạm vào từng yếu tố) VÀ nếu bạn có thể sống với khả năng trùng lặp:

new int[5].Select(o => (int)(rnd.NextDouble() * maxIndex)).Select(i => YourIEnum.ElementAt(i))

Để sử dụng, tôi có một danh sách 100.000 phần tử và do chúng được lấy từ DB, tôi đã giảm một nửa (hoặc tốt hơn) thời gian so với một rnd trong toàn bộ danh sách.

Có một danh sách lớn sẽ làm giảm tỷ lệ cược rất lớn cho các bản sao.


Giải pháp này có thể có các yếu tố lặp đi lặp lại !! Sự ngẫu nhiên trong danh sách lỗ có thể không.
AxelWass

Hừm. Thật. Tôi sử dụng nó ở đâu, điều đó không quan trọng. Chỉnh sửa câu trả lời để phản ánh điều đó.
Wolf5

-1

Điều này sẽ giải quyết vấn đề của bạn

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]);
                }

Mặc dù điều này có thể trả lời câu hỏi, bạn nên chỉnh sửa câu trả lời của mình để đưa ra lời giải thích về cách mã này trả lời câu hỏi. Điều này giúp cung cấp ngữ cảnh và làm cho câu trả lời của bạn hữu ích hơn nhiều cho người đọc trong tương lai.
Hoppeduppeanut
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.