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>
.
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>
.
Câu trả lời:
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 đó.
Sử dụng linq:
YourList.OrderBy(x => rnd.Next()).Take(5)
YourList
có 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 đó.
Đâ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.
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);
}
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.
Bạn có thể sử dụng nhưng việc đặt hàng sẽ diễn ra ở phía khách hàng
.AsEnumerable().OrderBy(n => Guid.NewGuid()).Take(5);
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.
var
kết quả trong needed
và available
cả hai là số nguyên, needed/available
luôn luôn là 0.
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 count
trở 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, 5
hay 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 - count
các mục từ nhóm hơn là thêm count
các mục. Vì lý do hiệu suất, tôi đã sử dụng source
thay vì sourceDict
lấ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 HashSet
sẽ 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 randoms
biế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.Next
có 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 ..
Đã 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 subset
các chỉ số ngẫu nhiên và sau đó nhổ chúng từ danh sách ban đầu.
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 HashSet
trong 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);
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);
}
}
}
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));
}
}
if (itemsList != null && k < n/2)
nghĩa là bên trong if
invertSet
luôn luôn false
có nghĩa là logic không bao giờ được sử dụng.
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
Đâ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.
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++;
}
Ở đâ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;
}
}
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 ;
}
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
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#
Nó khó hơn nhiều so với người ta nghĩ. Xem Điều tuyệt vời "Xáo trộn" từ Jeff.
Tôi đã viết một bài viết rất ngắn về chủ đề đó bao gồm mã C #:
Trả về tập hợp con ngẫu nhiên của các phần tử N của một mảng nhất định
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;
}
}
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
Đâ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;
}
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;
}
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 )
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
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.
Đ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]);
}