我想在保持订单的同时从非常大的列表中随机抽取样本。我在下面编写了脚本,但它需要.map(idx => ls(idx))
,这非常浪费。我可以通过辅助函数和尾递归看到一种提高效率的方法,但我觉得必须有一个我更缺失的简单解决方案。
有没有一种干净,更有效的方法呢?
import scala.util.Random
def sampledList[T](ls: List[T], sampleSize: Int) = {
Random
.shuffle(ls.indices.toList)
.take(sampleSize)
.sorted
.map(idx => ls(idx))
}
val sampleList = List("t","h","e"," ","q","u","i","c","k"," ","b","r","o","w","n")
// imagine the list is much longer though
sampledList(sampleList, 5) // List(e, u, i, r, n)
修改
看来我不清楚:我指的是维护值的顺序,而不是原始的List
集合。
答案 0 :(得分:5)
如果通过
维持值的顺序
您理解保持样本中的元素的顺序与ls
列表中的顺序相同,然后通过对原始解决方案的小修改,可以大大提高性能:
import scala.util.Random
def sampledList[T](ls: List[T], sampleSize: Int) = {
Random.shuffle(ls.zipWithIndex).take(sampleSize).sortBy(_._2).map(_._1)
}
此解决方案的复杂度为O(n + k * log(k)),其中n是列表的大小,k是样本大小,而您的解是O(n + k * log) (k)+ n * k)。
答案 1 :(得分:5)
这是一个(更复杂的)替代方案,具有O(n)
复杂性。您无法在复杂性方面做得更好(尽管您可以通过使用其他集合获得更好的性能,特别是具有恒定时间size
实现的集合。我做了一个快速的基准测试,表明加速是非常可观的。
import scala.util.Random
import scala.annotation.tailrec
def sampledList[T](ls: List[T], sampleSize: Int) = {
@tailrec
def rec(list: List[T], listSize: Int, sample: List[T], sampleSize: Int): List[T] = {
require(listSize >= sampleSize,
s"listSize must be >= sampleSize, but got listSize=$listSize and sampleSize=$sampleSize"
)
list match {
case hd :: tl =>
if (Random.nextInt(listSize) < sampleSize)
rec(tl, listSize-1, hd :: sample, sampleSize-1)
else rec(tl, listSize-1, sample, sampleSize)
case Nil =>
require(sampleSize == 0, // Should never happen
s"sampleSize must be zero at the end of processing, but got $sampleSize"
)
sample
}
}
rec(ls, ls.size, Nil, sampleSize).reverse
}
上述实现简单地遍历列表并根据设计为每个元素提供相同机会的概率保持(或不保持)当前元素。我的逻辑可能有一个流程,但乍一看对我来说似乎是合理的。
答案 2 :(得分:2)
这是另一个O(n)实现,它应该具有每个元素的统一概率:
implicit class SampleSeqOps[T](s: Seq[T]) {
def sample(n: Int, r: Random = Random): Seq[T] = {
assert(n >= 0 && n <= s.length)
val res = ListBuffer[T]()
val length = s.length
var samplesNeeded = n
for { (e, i) <- s.zipWithIndex } {
val p = samplesNeeded.toDouble / (length - i)
if (p >= r.nextDouble()) {
res += e
samplesNeeded -= 1
}
}
res.toSeq
}
}
我经常使用收藏品&gt; 100'000元素和性能似乎是合理的。
这可能与RégisJean-Gilles的回答相同,但我认为在这种情况下,必要的解决方案更具可读性。
答案 3 :(得分:1)
也许我不太明白,但是因为列表是不可改变的,所以你不必担心维持秩序&#39;因为从未触及原始列表。以下是不是足够了?
def sampledList[T](ls: List[T], sampleSize: Int) =
Random.shuffle(ls).take(sampleSize)
答案 4 :(得分:1)
虽然我之前的答案具有线性复杂性,但它确实存在需要两次传递的缺点,第一次传递对应于在执行任何其他操作之前计算长度的需要。除了影响运行时间之外,我们可能希望对一个非常大的集合进行采样,将整个集合一次加载到内存中是不实际的,也不是有效的,在这种情况下,我们希望能够使用一个简单的工作。迭代器。 碰巧的是,我们不需要发明任何东西来解决这个问题。有一个名为reservoir sampling的简单而聪明的算法就是这样做的(在我们迭代一个集合时构建一个样本,所有这些都在一次传递中)。通过微小的修改,我们还可以根据需要保留订单:
import scala.util.Random
def sampledList[T](ls: TraversableOnce[T], sampleSize: Int, preserveOrder: Boolean = false, rng: Random = new Random): Iterable[T] = {
val result = collection.mutable.Buffer.empty[(T, Int)]
for ((item, n) <- ls.toIterator.zipWithIndex) {
if (n < sampleSize) result += (item -> n)
else {
val s = rng.nextInt(n)
if (s < sampleSize) {
result(s) = (item -> n)
}
}
}
if (preserveOrder) {
result.sortBy(_._2).map(_._1)
}
else result.map(_._1)
}