【发布时间】:2014-04-28 01:52:01
【问题描述】:
我有一个布尔值数组,需要为 true 的值随机选择特定数量的索引。
生成索引数组最有效的方法是什么?
例如,
BitArray mask = GenerateSomeMask(length: 100000);
int[] randomIndices = RandomIndicesForTrue(mask, quantity: 10);
在这种情况下,randomIndices 的长度为 10。
【问题讨论】:
我有一个布尔值数组,需要为 true 的值随机选择特定数量的索引。
生成索引数组最有效的方法是什么?
例如,
BitArray mask = GenerateSomeMask(length: 100000);
int[] randomIndices = RandomIndicesForTrue(mask, quantity: 10);
在这种情况下,randomIndices 的长度为 10。
【问题讨论】:
有一种更快的方法可以做到这一点,只需扫描一次列表。
当您不知道文件中有多少行并且文件太大而无法放入内存时,请考虑从文本文件中随机选择一行。显而易见的解决方案是读取文件一次以计算行数,在 0 到 Count-1 的范围内选择一个随机数,然后再次读取文件直到所选的行号。这可行,但需要您读取文件两次。
更快的解决方案是读取第一行并将其保存为选定的行。您将选定的行替换为概率为 1/2 的下一行。当你阅读第三行时,你用概率1/3替换,等等。当你阅读整个文件时,你随机选择了一行,每一行被选中的概率相等。代码如下所示:
string selectedLine = null;
int numLines = 0;
Random rnd = new Random();
foreach (var line in File.ReadLines(filename))
{
++numLines;
double prob = 1.0/numLines;
if (rnd.Next() >= prob)
selectedLine = line;
}
现在,如果您想选择 2 行怎么办?您选择前两个。然后,在读取每一行时,它将替换两行之一的概率为 2/n,其中 n 是已读取的行数。如果您确定需要替换一行,则随机选择要替换的行。您可以按照相同的基本思想随机选择任意数量的行。例如:
string[] selectedLines = new int[M];
int numLines = 0;
Random rnd = new Random();
foreach (var line in File.ReadLines(filename))
{
++numLines;
if (numLines <= M)
{
selectedLines[numLines-1] = line;
}
else
{
double prob = (double)M/numLines;
if (rnd.Next() >= prob)
{
int ix = rnd.Next(M);
selectedLines[ix] = line;
}
}
}
你可以很容易地将它应用到你的BitArray:
int[] selected = new int[quantity];
int num = 0; // number of True items seen
Random rnd = new Random();
for (int i = 0; i < items.Length; ++i)
{
if (items[i])
{
++num;
if (num <= quantity)
{
selected[num-1] = i;
}
else
{
double prob = (double)quantity/num;
if (rnd.Next() > prob)
{
int ix = rnd.Next(quantity);
selected[ix] = i;
}
}
}
}
最后需要一些特殊情况代码来处理数组中没有 quantity 设置位的情况,但任何解决方案都需要它。
这会在BitArray 上进行一次传递,它使用的唯一额外内存是用于选定索引的列表。如果它没有明显快于 LINQ 版本,我会感到惊讶。
请注意,我使用概率计算来说明数学。您可以将第一个示例中的内部循环代码更改为:
if (rnd.Next(numLines+1) == numLines)
{
selectedLine = line;
}
++numLines;
您可以对其他示例进行类似的更改。这与概率计算的作用相同,并且应该执行得更快一些,因为它消除了每个项目的浮点除法。
【讨论】:
您可以使用两种方法:确定性和非确定性。第一个涉及找到集合中所有符合条件的元素,然后随机选择N;第二个涉及随机进入集合,直到找到N 符合条件的项目。
由于您的集合的大小在 100K 时是不可忽略的,并且您只想从中挑选一些,乍一看,应该考虑不确定性,因为它可以在实践中产生非常好的结果。 然而,由于无法保证 N 个 true 值甚至存在于集合中,因此不确定性可能会使您的程序陷入无限循环(灾难性较小,它可能只需要 a很长的时间 产生结果)。
因此,我建议采用确定性方法,即使您要通过资源使用来支付所需的保证。特别是,该操作将涉及辅助集合的就地排序;这实际上会抵消您使用BitArray 节省的空间。
抛开理论,让我们开始工作吧。处理此问题的标准方法是:
翻译成LINQ:
var results = mask
.Select((i, f) => Tuple.Create) // project into index/bool pairs
.Where(t => t.Item2) // keep only those where bool == true
.Select(t => t.Item1) // extract indices
.ToList() // prerequisite for next step
.Shuffle() // Fisher-Yates
.Take(quantity) // pick N
.ToArray(); // into an int[]
if (results.Length < quantity)
{
// not enough true values in input
}
【讨论】:
如果您有 10 个索引可供选择,您可以生成一个从 0 到 2^10 - 1 的随机数,并将其用作掩码。
【讨论】:
mask 包含 100,000 个元素,我只对 true 的 10 个元素的索引感兴趣。
true 值的预期/最小数量?这必然会涉及权衡,因此您需要选择正确的。
true 或false。