在循环中收集未知数量的结果
Posted
技术标签:
【中文标题】在循环中收集未知数量的结果【英文标题】:Collecting an unknown number of results in a loop 【发布时间】:2013-05-06 21:58:20 【问题描述】:如果事先不知道最终结果的数量,那么在 R 中循环收集结果的惯用方法是什么?这是一个玩具示例:
results = vector('integer')
i=1L
while (i < bigBigBIGNumber)
if (someCondition(i)) results = c(results, i)
i = i+1
results
这个例子的问题是(我假设)它将具有二次复杂度,因为向量需要在每次追加时重新分配。 (这是正确的吗?)我正在寻找避免这种情况的解决方案。
我找到了Filter
,但它需要预先生成1:bigBigBIGNumber
,我想避免这种情况以节省内存。 (问题:for (i in 1:N)
是否也预先生成了1:N
并将其保存在内存中?)
我可以制作像这样的链表:
results = list()
i=1L
while (i < bigBigBIGNumber)
if (someCondition(i)) results = list(results, i)
i = i+1
unlist(results)
(请注意,这不是串联。它正在构建一个类似list(list(list(1),2),3)
的结构,然后用unlist
展平。)
还有比这更好的方法吗?通常使用的惯用方式是什么? (我对 R 很陌生。)我正在寻找有关如何解决此类问题的建议。欢迎提出关于紧凑(易于编写)和快速代码的建议! (但我想专注于快速和内存效率。)
【问题讨论】:
c
函数用于扩展向量或列表。如果您可以估计大小,那么使用vector("integer", size)
进行分配将有助于降低扩展成本。
@DWin 是否有现有的工具可以按需以智能方式扩展阵列? (例如,一旦达到其容量,将预分配数组的大小加倍,并避免二次复杂度)
@Szabolcs,您为什么认为用list
替换c
会有所帮助?除非您预先分配一个列表,否则同样的问题仍然存在,不是吗?
@Arun 注意c
连接(触发重新分配向量),而list
我正在构建一个类似list(list(list(1),2),3)
的结构,一个链表。当放入循环时,后者具有线性复杂度,前者具有二次复杂度。您可以通过一个小型基准轻松验证这一点:将要附加的元素数量加倍,list
的时间加倍,c
的时间几乎翻了两番。这意味着对于“足够大”的结果,list
方法总是更快。当我写这个问题时,我没有意识到在这种情况下“足够大”......
现在,我希望 this answer 可能会有所帮助,因为 R 列表就像哈希映射数据结构..
【参考方案1】:
这是一种算法,它在输出列表填满时将其大小加倍,从而实现了一些线性的计算时间,如基准测试所示:
test <- function(bigBigBIGNumber = 1000)
n <- 10L
results <- vector("list", n)
m <- 0L
i <- 1L
while (i < bigBigBIGNumber)
if (runif(1) > 0.5)
m <- m + 1L
results[[m]] <- i
if (m == n)
results <- c(results, vector("list", n))
n <- n * 2L
i = i + 1L
unlist(results)
system.time(test(1000))
# user system elapsed
# 0.008 0.000 0.008
system.time(test(10000))
# user system elapsed
# 0.090 0.002 0.093
system.time(test(100000))
# user system elapsed
# 0.885 0.051 0.936
system.time(test(1000000))
# user system elapsed
# 9.428 0.339 9.776
【讨论】:
谢谢,这很实用,所以我会接受,但其他答案/cmets 也有助于理解人们认为 R 中的惯用语。 我猜线性度确实是循环中的开销(生成随机数,分配结果等);增长的时间等于(例如,对于 2^20 个元素)system.time( x = integer(1); for (i in 1:19) x <- c(x, integer(2^i)) )
(几分之一秒)。【参考方案2】:
大概有一个您愿意容忍的最大尺寸;预先分配并填充到该水平,然后在必要时进行修剪。这避免了无法满足双倍大小请求的风险,即使可能只需要少量额外的内存;它很早就失败了,并且只涉及一次而不是 log(n) 重新分配。这是一个具有最大大小的函数、一个生成函数和一个令牌,当没有任何东西可以生成时,生成函数返回该令牌。在返回之前我们最多可以得到 n 个结果
filln <-
function(n, FUN, ..., RESULT_TYPE="numeric", DONE_TOKEN=NA_real_)
results <- vector(RESULT_TYPE, n)
i <- 0L
while (i < n)
ans <- FUN(..., DONE_TOKEN=DONE_TOKEN)
if (identical(ans, DONE_TOKEN))
break
i <- i + 1L
results[[i]] <- ans
if (i == n)
warning("intolerably large result")
else length(results) <- i
results
这是一个生成器
fun <- function(thresh, DONE_TOKEN)
x <- rnorm(1)
if (x > thresh) DONE_TOKEN else x
在行动中
> set.seed(123L); length(filln(10000, fun, 3))
[1] 163
> set.seed(123L); length(filln(10000, fun, 4))
[1] 10000
Warning message:
In filln(10000, fun, 4) : intolerably large result
> set.seed(123L); length(filln(100000, fun, 4))
[1] 23101
我们可以通过与预先知道需要多少空间的东西进行比较来大致对开销进行基准测试
f1 <- function(n, FUN, ...)
i <- 0L
result <- numeric(n)
while (i < n)
i <- i + 1L
result[i] <- FUN(...)
result
这里我们检查单个结果的时间和值
> set.seed(123L); system.time(res0 <- filln(100000, fun, 4))
user system elapsed
0.944 0.000 0.948
> set.seed(123L); system.time(res1 <- f1(23101, fun, 4))
user system elapsed
0.688 0.000 0.689
> identical(res0, res1)
[1] TRUE
对于这个例子来说,它当然被简单的向量解决方案所掩盖
set.seed(123L); system.time(res2 <- rnorm(23101))
identical(res0, res2)
【讨论】:
【参考方案3】:如果您无法计算 1:bigBigNumber
,请计算条目,创建向量,然后填充它。
num <- 0L
i <- 0L
while (i < bigBigNumber)
if (someCondition(i)) num <- num + 1L
i <- i + 1L
result <- integer(num)
num <- 0L
while (i < bigBigNumber)
if (someCondition(i))
result[num] <- i
num <- num + 1L
i <- i + 1L
(此代码未经测试。)
如果您可以计算 1:bigBigBIGNumber
,这也可以:
我假设您想调用一个函数,而不是简单地添加索引本身。像这样的东西可能更接近你想要的:
values <- seq(bigBigBIGNumber)
sapply(values[someCondition(values)], my_function)
【讨论】:
+1 用于指出values[someCondition(values)]
中矢量化的潜在价值
当然,问题在于someCondition(i)
被计算两次。如果这是一个复杂的计算,那么执行两次可能会消耗掉任何性能提升。 I have a question along these lines,任何想法都将不胜感激。【参考方案4】:
更接近您列出的第二个:
results <- list()
for (i in ...)
...
results[[i]] <- ...
注意i
不必是integer
,可以是character
等。
此外,如果需要,您可以使用 results[[length(results)]] <- ...
,但如果您已经有迭代器,则可能不会。
【讨论】:
这是否解决了我询问的两个问题,即 1. 它是否预先生成所有要迭代的值(我不想保留所有值在内存中)和 2. 追加是否使其具有二次复杂性,即追加为results[[i]] <- ...
是否会导致重新分配整个列表?
一些基准测试表明它在我的评论中的 1. 点和 2. 点都失败了。但是,它也表明,这种追加到列表的方式比我尝试的链表方法更快(超过100000
),而且这种方式在R
中循环非常慢在使用 for
时,我通常会在内存不足之前“耗尽时间”。
如果效率非常重要,您可能希望查看基础R
之外的内容。 rcpp
浮现在脑海中。 cran.r-project.org/web/packages/Rcpp/index.html
在 R 中,任何赋值都将具有“二次复杂度”。在赋值完成之前,总是至少有一个临时副本,有时甚至更多。给出的建议是至少需要 3 倍的 RAM 来容纳工作区中最大的对象。 (每个“双精度”消耗大约 10 个字节。)
@DWin 你的意思是即使x=1:10; x[[3]] <- 10
也重新分配了 complete 数组?当然它不会这样做,因为您在其他评论中建议自己预先分配。二次复杂度不是分配,而是重复附加n
次。 (这将花费O(n^2)
时间。)以上是关于在循环中收集未知数量的结果的主要内容,如果未能解决你的问题,请参考以下文章
golang GoLang数据库SQL:从查询中选择未知数量的列。基准测试结果为db_test.go
如何在 PL/SQL 代码的 for 循环中创建游标并将结果批量收集到表中