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 设计的起源,而不是具体的实现。] 把它们放在一个环境中怎么样?这本质上是一个(散列)字典。您可以只为记录编号并在环境中调用它们12 等。这应该很快。然后最后转换成列表。 那是在帖子中描述的。但是以正确的顺序对键进行排序和提取值的效率有点低。 @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 pairlists,您可以在 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中的高效链表(有序集)的主要内容,如果未能解决你的问题,请参考以下文章

R中的高效方法是将新列添加到具有大数据集的数据框中

Redis跳跃表实现原理(加快在有序链表中的查找速度)

Redis跳跃表实现原理(加快在有序链表中的查找速度)

C ++中的高效链表?

如何在有序矩阵中高效搜索? [复制]

HashMap,LinkedHashMap,TreeMap的有序性