R中的高效链表(有序集)
Posted
技术标签:
【中文标题】R中的高效链表(有序集)【英文标题】:Efficient linked list (ordered set) in R 【发布时间】:2015-06-10 06:36:51 【问题描述】:从数据库游标读取记录时,通常事先不知道有多少行即将出现。这使得不可能预先分配正确大小的列表来存储这些对象。
当总大小未知时,将所有记录存储在列表中的有效方法是什么?基本列表类型很慢,因为每次添加元素时它都会复制整个列表:
x <- list()
for(i in 1:1e5)
x[[i]] <- list("foo" = rnorm(3), bar = TRUE)
环境效率更高,但它是一个映射而不是有序集合。所以我们需要将索引转换为字符串,然后对键进行排序以检索值,这似乎不是最理想的:
env <- new.env()
for(i in 1:1e5)
env[[sprintf("%09d", i)]] <- list("foo" = rnorm(3), bar = TRUE)
x <- lapply(sort(ls(env)), get, env, inherits = FALSE)
pairlist
应该是 R 中的链表,但是只要从 R 中附加一个元素,R 似乎就会将其强制转换为常规列表。
【问题讨论】:
哪里说pairlist
是一个链表?我认为这不太可能,因为它的性能特征(根据快速microbenchmark
测试,索引访问几乎肯定是恒定的,不是线性的)。 [文档中提到了“点对列表”,但这可能只是指pairlist
设计的起源,而不是具体的实现。]
把它们放在一个环境中怎么样?这本质上是一个(散列)字典。您可以只为记录编号并在环境中调用它们1
、2
等。这应该很快。然后最后转换成列表。
那是在帖子中描述的。但是以正确的顺序对键进行排序和提取值的效率有点低。
@KonradRudolph,pairlist (LISTSXP
) 确实是一个链表。有关 R 如何将它们用于解析器的示例,请参阅 github.com/wch/r-source/blob/trunk/src/main/gram.y#L1055-L1104。
@Jeroen 是的,很抱歉没有阅读这个问题! :( 您实际上可以更快地将环境转换为(排序的)列表,请参阅我的答案。
【参考方案1】:
这很慢:
> x <- list()
> for(i in 1:1e5)x[[i]]=list(foo=rnorm(3),bar=TRUE)
我放弃了等待。但这很快,几乎是瞬间完成的:
> x <- list()
> length(x)=1e5
> for(i in 1:1e5)x[[i]]=list(foo=rnorm(3),bar=TRUE)
所以我认为要走的路是每次将列表长度扩展 10000,并在到达最后一个元素并知道最终输出时将其修剪回来。
> length(x)=2e5 # extend by another 1e5
> for(i in 1:1e5)x[[i+1e5]]=list(foo=rnorm(3),bar=TRUE)
> length(x)=3e5 # and again... but this time only 100 more elts:
> for(i in 1:100)x[[i+2e5]]=list(foo=rnorm(3),bar=TRUE)
> length(x) = 2e5 + 100
另一种方法是每次需要更多元素时将列表大小加倍。
【讨论】:
嗯,我事先不知道1e5
的值。可能是 10 条记录,也可能是 1000 万条记录。这仍然会导致大量复制...
这就是为什么您可能希望每次都将列表长度加倍的原因。无论如何,现在我看看可能是骗局:***.com/questions/17046336/…【参考方案2】:
我认为你需要深入研究 C/C++ 才能最有效地做到这一点——R 并没有真正提供 R 语言级别的任何工具来修改适当的东西(包括配对列表,但环境除外),所以我会建议:
使用例如C++ STL 容器,它可以高效增长,然后将其强制返回到您需要的任何输出,或者
只需使用普通的旧 R pairlist
s,您可以在 C 级别与之交互并相当容易地进行扩展,然后在最后强制执行(如有必要)。
当然,您可以通过制作类似“可增长”向量的东西(例如跟踪容量,在必要时将其加倍,然后在最后缩小以适应)来使用普通 R 的方法(1),但通常在你需要这么低级的控制,值得一试 C/C++。
【讨论】:
您能否举个例子说明如何在 C 中扩展配对列表?【参考方案3】:我知道您可能不需要这个答案,所以这只是为了记录:环境并没有那么慢,您只需要将它们转换为“正确”列表。
这是您的代码,供参考。是的,这很慢。
system.time(
env <- new.env()
for(i in 1:1e5)
env[[sprintf("%09d", i)]] <- list("foo" = rnorm(3), bar = TRUE)
)
#> user system elapsed
#> 1.583 0.034 1.632
system.time(
x <- lapply(sort(ls(env)), get, env, inherits = FALSE)
)
#> user system elapsed
#> 1.595 0.014 1.629
一种稍微快一点的方式来放入元素:
system.time(
env <- new.env()
for(i in 1:1e5)
env[[as.character(i)]] <- list("foo" = rnorm(3), bar = TRUE)
)
#> user system elapsed
#> 1.039 0.023 1.072
不如预分配列表快,但几乎:
system.time(
l <- list()
length(l) <- 1e5
for(i in 1:1e5)
l[[i]] <- list("foo" = rnorm(3), bar = TRUE)
)
#> user system elapsed
#> 0.870 0.013 0.889
将环境转换为排序列表的更快方法:
system.time(
x <- as.list(env, sorted = FALSE)
x <- x[order(as.numeric(names(x)))]
)
#> user system elapsed
#> 0.073 0.000 0.074
如果这对您来说足够快,那么它比 C 代码和/或重新分配存储要容易得多。
【讨论】:
将环境转换为列表的巧妙方法!顺便说一句,为了填充环境或列表,大部分时间都花在了list("foo" = ...)
。使用恒定值可将我的计算机上的时间缩短 0.09 秒【参考方案4】:
不久前,我做了一些实验,用 R 中的配对列表与列表实现堆栈和队列,并将它们放在这个包中:https://github.com/wch/qstack。我在自述文件中添加了一些基准。
简短的版本:使用配对列表并不比使用列表快,并且随着列表的增长而加倍。另外:
直接修改任何其他 R 代码使用的配对列表是危险的,因为 R 假定配对列表是修改时复制的。 列表更节省内存,因为项不需要指向下一项的指针。 列表中的随机访问比配对列表中的访问要快得多。 如果您需要在配对列表中间插入或删除项目(使用 C),配对列表会更快。【讨论】:
【参考方案5】:下面是两个对列表的 c()
的 C 实现。
#include <Rinternals.h>
SEXP C_join_pairlist(SEXP x, SEXP y)
if(!isPairList(x) || !isPairList(y))
Rf_error("x and y must be pairlists");
//special case
if(x == R_NilValue)
return y;
//find the tail of x
SEXP tail = x;
while(CDR(tail) != R_NilValue)
tail = CDR(tail);
//append to tail
SETCDR(tail, y);
return x;
还有一个简单的 R 包装器:
join_pairlist <- function(x, values)
.Call(C_join_pairlist, x, values)
你可以这样使用它:
> x <- pairlist("foo", "bar")
> y <- pairlist("baz", "bla", "boe")
> x <- join_pairlist(x,y)
[1] TRUE
> print(x)
[[1]]
[1] "foo"
[[2]]
[1] "bar"
[[3]]
[1] "baz"
[[4]]
[1] "bla"
[[5]]
[1] "boe"
这很有效,但也很危险,因为它会更改 x
的值而不复制它。这种方式很容易意外引入循环引用。
【讨论】:
【参考方案6】:我在https://***.com/a/32870310/264177 的回答中有一个重复加倍列表实现的示例。它不是作为链表实现的,而是作为扩展数组实现的。对于大型数据集,它比其他替代方案快很多。
我实际上是为您在此处描述的相同问题构建的,存储了大量从数据库中检索到的项目,而您事先不知道项目的数量。
【讨论】:
以上是关于R中的高效链表(有序集)的主要内容,如果未能解决你的问题,请参考以下文章