我需要一个快速算法来从通用列表中选择 5 个随机元素。例如,我想从 a 中获取 5 个随机元素List<string>
。
30 回答
使用 linq:
YourList.OrderBy(x => rnd.Next()).Take(5)
public static List<T> GetRandomElements<T>(this IEnumerable<T> list, int elementsCount)
{
return list.OrderBy(arg => Guid.NewGuid()).Take(elementsCount).ToList();
}
这实际上是一个比听起来更难的问题,主要是因为许多数学上正确的解决方案实际上无法让您达到所有可能性(更多内容见下文)。
首先,这里有一些易于实现、正确的如果你有一个真正的随机数生成器:
(0) 凯尔的答案,即 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,这可以更快。如果你保持 s 排序并跟踪它有哪些连续的间隔,你可以在 O(k log k) 中实现它,但它的工作量更大。
@Kyle - 你是对的,我同意你的回答。我一开始就匆忙阅读它,并错误地认为您是在指示以固定概率 k/n 顺序选择每个元素,这本来是错误的 - 但您的自适应方法对我来说似乎是正确的。对于那个很抱歉。
好的,现在对于踢球者:渐近(对于固定的 k,n 增长),有 n^k/k!从 n 个元素中选择 k 个元素子集 [这是 (n 选择 k) 的近似值]。如果 n 很大,并且 k 不是很小,那么这些数字就很大。在任何标准的 32 位随机数生成器中,您希望的最佳循环长度是 2^32 = 256^4。因此,如果我们有一个包含 1000 个元素的列表,并且我们想随机选择 5 个,那么标准的随机数生成器不可能满足所有可能性。但是,只要您对适用于较小集合的选择感到满意,并且总是“看起来”随机,那么这些算法应该没问题。
附录:写完后,我意识到正确实施想法(2)很棘手,所以我想澄清这个答案。要获得 O(k log k) 时间,您需要一个支持 O(log m) 搜索和插入的类似数组的结构 - 平衡二叉树可以做到这一点。使用这样的结构来构建一个名为 s 的数组,这里是一些伪 Python:
# 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
我建议通过几个示例案例来看看它如何有效地实现上述英文解释。
我认为选择的答案是正确的并且非常甜蜜。不过,我以不同的方式实现了它,因为我还希望结果是随机顺序的。
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);
}
我刚刚遇到了这个问题,更多的谷歌搜索让我遇到了随机洗牌的问题:http ://en.wikipedia.org/wiki/Fisher-Yates_shuffle
要完全随机打乱您的列表(就地),请执行以下操作:
要对包含 n 个元素(索引 0..n-1)的数组 a 进行洗牌:
for i from n − 1 downto 1 do
j ← random integer with 0 ≤ j ≤ i
exchange a[j] and a[i]
如果只需要前 5 个元素,那么不需要从 n-1 一直运行 i 到 1,只需运行到 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) 运行时间。
最后,对于真正的坚持者,如果 (n == k),您应该停在 1,而不是 nk,因为随机选择的整数将始终为 0。
您可以使用它,但订购将在客户端发生
.AsEnumerable().OrderBy(n => Guid.NewGuid()).Take(5);
来自Dragons in the Algorithm,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--;
}
该算法将选择项目列表的唯一索引。
正在考虑@JohnShedletsky 对关于(释义)的公认答案的评论:
你应该能够在 O(subset.Length) 中做到这一点,而不是 O(originalList.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);
}
}
}
12 年过去了,这个问题仍然存在,我没有找到我喜欢的 Kyle 解决方案的实现,所以这里是:
public IEnumerable<T> TakeRandom<T>(IEnumerable<T> collection, int take)
{
var random = new Random();
var available = collection.Count();
var needed = take;
foreach (var item in collection)
{
if (random.Next(available) < needed)
{
needed--;
yield return item;
if (needed == 0)
{
break;
}
}
available--;
}
}
从一个组中选择 N个随机项目不应该与order有任何关系!随机性是关于不可预测性,而不是在一个群体中改变位置。所有处理某种排序的答案肯定比不处理的答案效率低。由于效率是这里的关键,我将发布一些不会过多改变项目顺序的内容。
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 个项目,那么它应该不可预测地产生,等。其次,当随机项目的计数是返回是原组的一半以上,那么它更容易删除count
source.Count
1, 2, 3, 4, 5
1, 3, 4, 2, 5
1, 2, 3, 4, 5
1, 2, 3, 4
1, 3, 5, 2
2, 3, 5, 4
source.Count - count
组中的项目而不是添加count
项目。出于性能原因,我使用remove 方法source
而不是sourceDict
在 remove 方法中获取随机索引。
因此,如果您有 { 1, 2, 3, 4 },这可能会以 { 1, 2, 3 }, { 3, 4, 1 } 等 3 个项目结束。
3)如果考虑到原始组中的重复项,您需要从组中真正不同的随机值,那么您可以使用与上述相同的方法,但 aHashSet
会比字典轻。
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
变量设置为 a 是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 方法更胜一筹- 简短、甜蜜和简单。
我结合了上述几个答案来创建一个懒惰评估的扩展方法。我的测试表明,Kyle 的方法 (Order(N)) 比 drzaus 使用集合来提出要选择的随机索引 (Order(K)) 慢很多倍。前者对随机数生成器执行更多调用,并在项目上迭代更多次。
我的实施目标是:
1) 如果给定的 IEnumerable 不是 IList,则不要实现完整列表。如果给我一个包含无数个项目的序列,我不想耗尽内存。使用 Kyle 的方法获得在线解决方案。
2)如果我能看出它是一个 IList,请使用 drzaus 的方法,但要有所改变。如果 K 超过 N 的一半,我会冒着颠簸的风险,因为我一次又一次地选择许多随机索引并且不得不跳过它们。因此,我编写了一个不保留的索引列表。
3) 我保证物品将按照遇到的顺序退回。凯尔的算法不需要改变。drzaus 的算法要求我不要按照选择随机索引的顺序发出项目。我将所有索引收集到一个 SortedSet 中,然后按排序索引顺序发出项目。
4)如果K与N相比较大并且我反转了集合的意义,那么我枚举所有项目并测试索引是否不在集合中。这意味着我失去了 Order(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# 的Random。(FastRandom由 Colin Green 编写,属于 SharpNEAT 的一部分。它的周期为 2^128-1,优于许多 RNG。)
以下是单元测试:
[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));
}
}
从@ers's answer扩展,如果有人担心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
在这里,您有一个基于Fisher-Yates Shuffle的实现,其算法复杂度为 O(n),其中 n 是子集或样本大小,而不是列表大小,正如 John Shedletsky 指出的那样。
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;
}
}
我使用的简单解决方案(可能不适合大型列表):将列表复制到临时列表中,然后在循环中从临时列表中随机选择项目并将其放入选定项目列表中,同时将其从临时列表中删除(因此不能重新选择)。
例子:
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++;
}
这是我在第一次剪辑时能想到的最好的:
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 范围内的随机列表 - 总列表计数然后简单地将这些项目拉到列表中似乎是最好的方法,但使用字典来确保唯一性是我仍在考虑的事情。
另请注意,我使用了一个字符串列表,根据需要替换。
根据凯尔的回答,这是我的 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 ;
}
这种方法可能相当于凯尔的方法。
假设您的列表大小为 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--;
}
}
奇迹般有效 :)
——亚历克斯·吉尔伯特
以下是三种不同方法的基准:
- Kyle接受的答案的实施。
- 一种基于随机索引选择和 HashSet 重复过滤的方法,来自drzaus。
- Jesús López发布了一种更具学术性的方法,称为Fisher-Yates shuffle。
测试将包括使用多种不同的列表大小和选择大小对性能进行基准测试。
我还包括了对这三种方法的标准偏差的测量,即随机选择的分布情况。
简而言之,drzaus的简单解决方案似乎是这三个中最好的。选择的答案很棒而且很优雅,但效率不高,因为时间复杂度是基于样本大小而不是选择大小。因此,如果您从长列表中选择少量项目,则将花费更多时间。当然,它仍然比基于完全重新排序的解决方案表现更好。
奇怪的是,这个O(n)
时间复杂度问题是真实的,即使你只在实际返回一个项目时才触摸列表,就像我在我的实现中所做的那样。我唯一能做的就是这Random.Next()
很慢,如果你为每个选定的项目只生成一个随机数,那么性能会得到好处。
而且,同样有趣的是,Kyle 解决方案的 StdDev 相对而言要高得多。我不知道为什么; 也许问题出在我的实施中。
很抱歉现在开始的长代码和输出;但我希望它有点启发性。另外,如果您在测试或实现中发现任何问题,请告诉我,我会修复它。
static void Main()
{
BenchmarkRunner.Run<Benchmarks>();
new Benchmarks() { ListSize = 100, SelectionSize = 10 }
.BenchmarkStdDev();
}
[MemoryDiagnoser]
public class Benchmarks
{
[Params(50, 500, 5000)]
public int ListSize;
[Params(5, 10, 25, 50)]
public int SelectionSize;
private Random _rnd;
private List<int> _list;
private int[] _hits;
[GlobalSetup]
public void Setup()
{
_rnd = new Random(12345);
_list = Enumerable.Range(0, ListSize).ToList();
_hits = new int[ListSize];
}
[Benchmark]
public void Test_IterateSelect()
=> Random_IterateSelect(_list, SelectionSize).ToList();
[Benchmark]
public void Test_RandomIndices()
=> Random_RandomIdices(_list, SelectionSize).ToList();
[Benchmark]
public void Test_FisherYates()
=> Random_FisherYates(_list, SelectionSize).ToList();
public void BenchmarkStdDev()
{
RunOnce(Random_IterateSelect, "IterateSelect");
RunOnce(Random_RandomIdices, "RandomIndices");
RunOnce(Random_FisherYates, "FisherYates");
void RunOnce(Func<IEnumerable<int>, int, IEnumerable<int>> method, string methodName)
{
Setup();
for (int i = 0; i < 1000000; i++)
{
var selected = method(_list, SelectionSize).ToList();
Debug.Assert(selected.Count() == SelectionSize);
foreach (var item in selected) _hits[item]++;
}
var stdDev = GetStdDev(_hits);
Console.WriteLine($"StdDev of {methodName}: {stdDev :n} (% of average: {stdDev / (_hits.Average() / 100) :n})");
}
double GetStdDev(IEnumerable<int> hits)
{
var average = hits.Average();
return Math.Sqrt(hits.Average(v => Math.Pow(v - average, 2)));
}
}
public IEnumerable<T> Random_IterateSelect<T>(IEnumerable<T> collection, int needed)
{
var count = collection.Count();
for (int i = 0; i < count; i++)
{
if (_rnd.Next(count - i) < needed)
{
yield return collection.ElementAt(i);
if (--needed == 0)
yield break;
}
}
}
public IEnumerable<T> Random_RandomIdices<T>(IEnumerable<T> list, int needed)
{
var selectedItems = new HashSet<T>();
var count = list.Count();
while (needed > 0)
if (selectedItems.Add(list.ElementAt(_rnd.Next(count))))
needed--;
return selectedItems;
}
public IEnumerable<T> Random_FisherYates<T>(IEnumerable<T> list, int sampleSize)
{
var count = list.Count();
if (sampleSize > count) throw new ArgumentException("sampleSize may not be greater than list count", "sampleSize");
var indices = new Dictionary<int, int>(); int index;
for (int i = 0; i < sampleSize; i++)
{
int j = _rnd.Next(i, count);
if (!indices.TryGetValue(j, out index)) index = j;
yield return list.ElementAt(index);
if (!indices.TryGetValue(i, out index)) index = i;
indices[j] = index;
}
}
}
输出:
| Method | ListSize | Select | Mean | Error | StdDev | Gen 0 | Allocated |
|-------------- |--------- |------- |------------:|----------:|----------:|-------:|----------:|
| IterateSelect | 50 | 5 | 711.5 ns | 5.19 ns | 4.85 ns | 0.0305 | 144 B |
| RandomIndices | 50 | 5 | 341.1 ns | 4.48 ns | 4.19 ns | 0.0644 | 304 B |
| FisherYates | 50 | 5 | 573.5 ns | 6.12 ns | 5.72 ns | 0.0944 | 447 B |
| IterateSelect | 50 | 10 | 967.2 ns | 4.64 ns | 3.87 ns | 0.0458 | 220 B |
| RandomIndices | 50 | 10 | 709.9 ns | 11.27 ns | 9.99 ns | 0.1307 | 621 B |
| FisherYates | 50 | 10 | 1,204.4 ns | 10.63 ns | 9.94 ns | 0.1850 | 875 B |
| IterateSelect | 50 | 25 | 1,358.5 ns | 7.97 ns | 6.65 ns | 0.0763 | 361 B |
| RandomIndices | 50 | 25 | 1,958.1 ns | 15.69 ns | 13.91 ns | 0.2747 | 1298 B |
| FisherYates | 50 | 25 | 2,878.9 ns | 31.42 ns | 29.39 ns | 0.3471 | 1653 B |
| IterateSelect | 50 | 50 | 1,739.1 ns | 15.86 ns | 14.06 ns | 0.1316 | 629 B |
| RandomIndices | 50 | 50 | 8,906.1 ns | 88.92 ns | 74.25 ns | 0.5951 | 2848 B |
| FisherYates | 50 | 50 | 4,899.9 ns | 38.10 ns | 33.78 ns | 0.4349 | 2063 B |
| IterateSelect | 500 | 5 | 4,775.3 ns | 46.96 ns | 41.63 ns | 0.0305 | 144 B |
| RandomIndices | 500 | 5 | 327.8 ns | 2.82 ns | 2.50 ns | 0.0644 | 304 B |
| FisherYates | 500 | 5 | 558.5 ns | 7.95 ns | 7.44 ns | 0.0944 | 449 B |
| IterateSelect | 500 | 10 | 5,387.1 ns | 44.57 ns | 41.69 ns | 0.0458 | 220 B |
| RandomIndices | 500 | 10 | 648.0 ns | 9.12 ns | 8.54 ns | 0.1307 | 621 B |
| FisherYates | 500 | 10 | 1,154.6 ns | 13.66 ns | 12.78 ns | 0.1869 | 889 B |
| IterateSelect | 500 | 25 | 6,442.3 ns | 48.90 ns | 40.83 ns | 0.0763 | 361 B |
| RandomIndices | 500 | 25 | 1,569.6 ns | 15.79 ns | 14.77 ns | 0.2747 | 1298 B |
| FisherYates | 500 | 25 | 2,726.1 ns | 25.32 ns | 22.44 ns | 0.3777 | 1795 B |
| IterateSelect | 500 | 50 | 7,775.4 ns | 35.47 ns | 31.45 ns | 0.1221 | 629 B |
| RandomIndices | 500 | 50 | 2,976.9 ns | 27.11 ns | 24.03 ns | 0.6027 | 2848 B |
| FisherYates | 500 | 50 | 5,383.2 ns | 36.49 ns | 32.35 ns | 0.8163 | 3870 B |
| IterateSelect | 5000 | 5 | 45,208.6 ns | 459.92 ns | 430.21 ns | - | 144 B |
| RandomIndices | 5000 | 5 | 328.7 ns | 5.15 ns | 4.81 ns | 0.0644 | 304 B |
| FisherYates | 5000 | 5 | 556.1 ns | 10.75 ns | 10.05 ns | 0.0944 | 449 B |
| IterateSelect | 5000 | 10 | 49,253.9 ns | 420.26 ns | 393.11 ns | - | 220 B |
| RandomIndices | 5000 | 10 | 642.9 ns | 4.95 ns | 4.13 ns | 0.1307 | 621 B |
| FisherYates | 5000 | 10 | 1,141.9 ns | 12.81 ns | 11.98 ns | 0.1869 | 889 B |
| IterateSelect | 5000 | 25 | 54,044.4 ns | 208.92 ns | 174.46 ns | 0.0610 | 361 B |
| RandomIndices | 5000 | 25 | 1,480.5 ns | 11.56 ns | 10.81 ns | 0.2747 | 1298 B |
| FisherYates | 5000 | 25 | 2,713.9 ns | 27.31 ns | 24.21 ns | 0.3777 | 1795 B |
| IterateSelect | 5000 | 50 | 54,418.2 ns | 329.62 ns | 308.32 ns | 0.1221 | 629 B |
| RandomIndices | 5000 | 50 | 2,886.4 ns | 36.53 ns | 34.17 ns | 0.6027 | 2848 B |
| FisherYates | 5000 | 50 | 5,347.2 ns | 59.45 ns | 55.61 ns | 0.8163 | 3870 B |
StdDev of IterateSelect: 671.88 (% of average: 0.67)
StdDev of RandomIndices: 296.07 (% of average: 0.30)
StdDev of FisherYates: 280.47 (% of average: 0.28)
这比人们想象的要难得多。请参阅 Jeff 的精彩文章“洗牌”。
我确实写了一篇关于该主题的非常短的文章,包括 C# 代码:
返回给定数组的 N 个元素的随机子集
目标:从集合源中选择 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;
}
}
我最近在我的项目中使用了类似于Tyler 的第 1 点的想法。
我正在加载一堆问题并随机选择五个。排序是使用IComparer实现的。
a 所有问题都加载到 QuestionSorter 列表中,然后使用列表的 Sort 函数和选择的前 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
为什么不这样:
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#
这是我的方法(全文在这里http://krkadev.blogspot.com/2010/08/random-numbers-without-repetition.html)。
它应该在 O(K) 而不是 O(N) 中运行,其中 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;
}
这不像公认的解决方案那样优雅或高效,但写起来很快。首先,随机排列数组,然后选择前 K 个元素。在蟒蛇中,
import numpy
N = 20
K = 5
idx = np.arange(N)
numpy.random.shuffle(idx)
print idx[:K]
我会使用扩展方法。
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;
}
将 LINQ 与大型列表一起使用(当触摸每个元素的成本很高时)并且如果您可以忍受重复的可能性:
new int[5].Select(o => (int)(rnd.NextDouble() * maxIndex)).Select(i => YourIEnum.ElementAt(i))
对于我的使用,我有一个包含 100.000 个元素的列表,并且由于它们是从数据库中提取的,与整个列表中的 rnd 相比,我的时间大约减半(或更好)。
拥有一个大列表将大大降低重复的几率。
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
复杂度:O(count 2 )
当 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
这将解决您的问题
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]);
}