本文的思路来源于Swift 算法俱乐部的一篇文章,从一组数据中选取n个数,结合之前项目中碰到的问题,感觉很有用,能大大提升工作效率,在此记录加复习一下。(●ˇ∀ˇ●)
首先看样例:从n个项的集合中随机选择k个项。
假设你有一副52张牌,你需要随机抽取10张牌。
方法一:
最开始自己用的笨办法是复制这个集合到一个新集合中,之后随机k次,每取出一个值之后就从新集合中移除这个值,保证随机出来的值不会重复。
代码
/// <summary>
/// 从list中随机取出count个值
/// </summary>
/// <param name="value"></param>
/// <param name="count"></param>
static List<int> SampleAlgorithm(List<int> value,int count)
{
List<int> tempList = new List<int>();
tempList.AddRange(value);
List<int> targetValue= new List<int>();
Random random = new Random();
for (int i = 0; i < count; i++)
{
int randomIndex= random.Next(0,tempList.Count);
Console.WriteLine(randomIndex);
if (randomIndex >= tempList.Count)
break;
targetValue.Add(tempList[randomIndex]);
tempList.RemoveAt(randomIndex);
}
return targetValue;
}
方法二:
方法二参考文章中示例,通过用C#的方法实现,这个方法的原理是通过每次从当前索引到总数之间取一个随机数,之后将这个索引处的值和当前索引处值进行交换,最后的列表前n位就是随机出来的数,但是在C#中因为List是对象引用,所以这样也会修改原集合中的值,所以我们在随机之前需要先创建一个新的集合。
static List<int> Method2(List<int> value, int count)
{
List<int> tempValue = new List<int>();
tempValue.AddRange(value);
Random random = new Random();
for (int i = 0; i < count; i++)
{
if (i >= tempValue.Count)
break;
int randomIndex=random.Next(i, tempValue.Count);
if(randomIndex != i)
{
Swap(tempValue, randomIndex, i);
}
}
if(count>= tempValue.Count)
count= tempValue.Count;
return tempValue.GetRange(0, count);
}
方法三:
被称为“水库抽样”(Reservoir Sampling),他的原理有两步
- 使用原始数组中的
k个
元素填充result
数组。 这被称为“水库”。 - 用剩余池中的元素随机替换水库中的元素
它的最大优点是它可以用于太大而无法容纳在内存中的数组,即使你不知道数组的大小是多少(在Swift中这可能类似于读取文件元素的懒惰生成器)。这种算法可以保证原始数组的数据不发生改变,但是他会比较费时和费空间。
static List<int> Method3(List<int> value, int count)
{
int examined=0, selected=0;
List<int> result=new List<int>();
Random random = new Random();
while (selected < count)
{
double r = random.NextDouble();
int leftToexamine = value.Count - examined;
int leftToAdd = count - selected;
if (leftToexamine * r < leftToAdd)
{
selected++;
result.Add(value[examined]);
}
examined++;
}
return result;
}
参考资料: