【问题标题】:Append an object to a list in R in amortized constant time, O(1)?在摊销常数时间内将对象附加到 R 中的列表,O(1)?
【发布时间】:2011-01-27 01:25:17
【问题描述】:

如果我有一些 R 列表 mylist,您可以像这样附加一个项目 obj

mylist[[length(mylist)+1]] <- obj

但肯定有一些更紧凑的方法。当我是 R 的新手时,我尝试像这样写 lappend()

lappend <- function(lst, obj) {
    lst[[length(lst)+1]] <- obj
    return(lst)
}

但当然,由于 R 的按名称调用语义,这不起作用(lst 在调用时被有效复制,因此对lst 的更改在lappend() 范围之外不可见。我认识你可以在 R 函数中进行环境黑客攻击,以达到函数范围之外并改变调用环境,但这似乎是编写简单附加函数的大锤。

任何人都可以提出一种更漂亮的方法吗?如果它适用于向量和列表,则加分。

【问题讨论】:

  • R 具有函数式语言中经常出现的不可变数据特性,讨厌这样说,但我认为你只需要处理它。它有它的优点和缺点
  • 当您说“按名称调用”时,您的真正意思是“按值调用”,对吧?
  • 不,绝对不是按值调用,否则不会有问题。 R 实际上使用按需调用 (en.wikipedia.org/wiki/Evaluation_strategy#Call_by_need)。
  • 一个好主意是预先分配你的向量/列表: N = 100 mylist = vector('list', N) for (i in 1:N) { #mylist[[i]] = ... } 避免在 R 中“增长”对象。
  • 无意间在这里找到了答案,stackoverflow.com/questions/17046336/…这么简单的算法实现起来太难了!

标签: r performance list append big-o


【解决方案1】:

mylist<-list(1,2,3) mylist<-c(mylist,list(5))

所以我们可以使用上面的代码轻松附加元素/对象

【讨论】:

    【解决方案2】:

    为了验证,我运行了@Cron 提供的基准代码。有一个主要区别(除了在较新的 i7 处理器上运行得更快之外):by_index 现在的性能几乎与list_ 一样好:

    Unit: milliseconds
                  expr        min         lq       mean     median         uq
        env_with_list_ 167.882406 175.969269 185.966143 181.817187 185.933887
                    c_ 485.524870 501.049836 516.781689 518.637468 537.355953
                 list_   6.155772   6.258487   6.544207   6.269045   6.290925
              by_index   9.290577   9.630283   9.881103   9.672359  10.219533
               append_ 505.046634 543.319857 542.112303 551.001787 553.030110
     env_as_container_ 153.297375 154.880337 156.198009 156.068736 156.800135
    

    这里的参考是从@Cron 的答案逐字复制的基准代码(以防他以后更改内容):

    n = 1e+4
    library(microbenchmark)
    ### Using environment as a container
    lPtrAppend <- function(lstptr, lab, obj) {lstptr[[deparse(substitute(lab))]] <- obj}
    ### Store list inside new environment
    envAppendList <- function(lstptr, obj) {lstptr$list[[length(lstptr$list)+1]] <- obj}
    
    microbenchmark(times = 5,
            env_with_list_ = {
                listptr <- new.env(parent=globalenv())
                listptr$list <- NULL
                for(i in 1:n) {envAppendList(listptr, i)}
                listptr$list
            },
            c_ = {
                a <- list(0)
                for(i in 1:n) {a = c(a, list(i))}
            },
            list_ = {
                a <- list(0)
                for(i in 1:n) {a <- list(a, list(i))}
            },
            by_index = {
                a <- list(0)
                for(i in 1:n) {a[length(a) + 1] <- i}
                a
            },
            append_ = {
                a <- list(0)
                for(i in 1:n) {a <- append(a, i)}
                a
            },
            env_as_container_ = {
                listptr <- new.env(parent=globalenv())
                for(i in 1:n) {lPtrAppend(listptr, i, i)}
                listptr
            }
    )
    

    【讨论】:

    • 见我上面的评论。 list_ 不提供与其他相同的结果。所以这个比较是无效的!
    【解决方案3】:

    还有list.append来自rlistlink to the documentation

    require(rlist)
    LL <- list(a="Tom", b="Dick")
    list.append(LL,d="Pam",f=c("Joe","Ann"))
    

    非常简单高效。

    【讨论】:

    • 在我看来不像 R...Python?
    • 我进行了编辑并试了一下:速度太慢了。最好使用c()list-方法。两者都快得多。
    • 查看rlist::list.append() 的代码,它本质上是base::c() 的包装。
    【解决方案4】:

    如果是字符串列表,只需使用c() 函数:

    R> LL <- list(a="tom", b="dick")
    R> c(LL, c="harry")
    $a
    [1] "tom"
    
    $b
    [1] "dick"
    
    $c
    [1] "harry"
    
    R> class(LL)
    [1] "list"
    R> 
    

    这也适用于矢量,所以我可以获得奖励积分吗?

    编辑(2015 年 2 月 01 日): 这篇文章即将迎来它的五岁生日。一些善良的读者不断重复它的任何缺点,所以一定要看看下面的一些 cmets。对list 类型的一项建议:

    newlist <- list(oldlist, list(someobj))
    

    一般来说,R 类型很难为所有类型和用途使用一个且只有一个习语。

    【讨论】:

    • 这不追加...它连接。在调用 C(LL, c="harry") 之后,LL 仍然有两个元素。
    • 只需重新分配给 LL:LL &lt;- c(LL, c="harry")
    • 这仅适用于字符串。如果 a、b 和 c 是整数向量,则行为完全不同。
    • @Dirk:您的括号嵌套方式与我不同。我对c() 的调用有两个参数:我要附加到的列表,即list(a=3, b=c(4, 5)),以及我要附加的项目,即c=c(6, 7)。如果您使用我的方法,您会看到附加了 2 列表项(67,名称为 c1c2)而不是单个 2 元素向量命名为 c 显然是有意的!
    • 结论是mylist &lt;- list(mylist, list(obj))?如果是,修改答案会很好
    【解决方案5】:

    这是一个非常有趣的问题,我希望我下面的想法可以为它提供一种解决方法。此方法确实提供了一个没有索引的平面列表,但它确实有 list 和 unlist 以避免嵌套结构。我不确定速度,因为我不知道如何对其进行基准测试。

    a_list<-list()
    for(i in 1:3){
      a_list<-list(unlist(list(unlist(a_list,recursive = FALSE),list(rnorm(2))),recursive = FALSE))
    }
    a_list
    
    [[1]]
    [[1]][[1]]
    [1] -0.8098202  1.1035517
    
    [[1]][[2]]
    [1] 0.6804520 0.4664394
    
    [[1]][[3]]
    [1] 0.15592354 0.07424637
    

    【讨论】:

    • 我想补充的是它确实提供了一个两级嵌套列表,但仅此而已。 list 和 unlist 的工作方式对我来说不是很清楚,但这是测试代码的结果
    【解决方案6】:

    OP(在 2012 年 4 月更新的问题修订版中)有兴趣知道是否有办法在摊销的常数时间内添加到列表中,例如可以使用 C++ vector&lt;&gt; 容器来完成.到目前为止,这里的最佳答案仅显示给定固定大小问题的各种解决方案的相对执行时间,但没有直接解决任何各种解决方案的algorithmic efficiency。许多答案下方的评论讨论了一些解决方案的算法效率,但是迄今为止的所有情况(截至 2015 年 4 月)他们来了得出错误的结论。

    算法效率捕获增长特征,无论是时间(执行时间)还是空间(消耗的内存量)随着问题规模的增长。给定一个固定大小的问题,对各种解决方案进行性能测试并不能解决各种解决方案的增长率。 OP 有兴趣知道是否有办法在“摊销常数时间”中将对象附加到 R 列表中。那是什么意思?为了解释,首先让我描述一下“恒定时间”:

    • 恒定O(1)增长:

      如果执行给定任务所需的时间保持不变与问题的大小加倍相同,那么我们说该算法表现出恒定时间 em> 增长,或用“大 O”表示法表示,表现出 O(1) 时间增长。当 OP 说“摊销”恒定时间时,他的意思是“从长远来看”......即,如果执行单个操作偶尔需要比正常时间更长的时间(例如,如果预先分配的缓冲区用尽并且偶尔需要调整到更大的缓冲区大小),只要长期平均性能是常数时间,我们仍然称其为 O(1)。

      为了比较,我还将描述“线性时间”和“二次时间”:

    • 线性O(n)增长:

      如果执行给定任务所需的时间加倍是问题的大小加倍,那么我们说算法表现出线性时间 , 或 O(n) 增长。

    • 二次O(n2)增长:

      如果执行给定任务所需的时间按问题大小的平方增加,我们称该算法表现出二次时间,或O( n2) 增长。

    还有许多其他效率等级的算法;我遵从Wikipedia article 的进一步讨论。

    感谢@CronAcronis 的回答,因为我是 R 新手,很高兴有一个完全构建的代码块来对本页面上提供的各种解决方案进行性能分析。我借用他的代码进行分析,我在下面复制(包装在一个函数中):

    library(microbenchmark)
    ### Using environment as a container
    lPtrAppend <- function(lstptr, lab, obj) {lstptr[[deparse(substitute(lab))]] <- obj}
    ### Store list inside new environment
    envAppendList <- function(lstptr, obj) {lstptr$list[[length(lstptr$list)+1]] <- obj} 
    runBenchmark <- function(n) {
        microbenchmark(times = 5,  
            env_with_list_ = {
                listptr <- new.env(parent=globalenv())
                listptr$list <- NULL
                for(i in 1:n) {envAppendList(listptr, i)}
                listptr$list
            },
            c_ = {
                a <- list(0)
                for(i in 1:n) {a = c(a, list(i))}
            },
            list_ = {
                a <- list(0)
                for(i in 1:n) {a <- list(a, list(i))}
            },
            by_index = {
                a <- list(0)
                for(i in 1:n) {a[length(a) + 1] <- i}
                a
            },
            append_ = { 
                a <- list(0)    
                for(i in 1:n) {a <- append(a, i)} 
                a
            },
            env_as_container_ = {
                listptr <- new.env(parent=globalenv())
                for(i in 1:n) {lPtrAppend(listptr, i, i)} 
                listptr
            }   
        )
    }
    

    @CronAcronis 发布的结果显然表明a &lt;- list(a, list(i)) 方法最快,至少对于 10000 的问题大小,但单个问题大小的结果并不能解决解决方案的增长。为此,我们需要运行至少两个具有不同问题大小的分析测试:

    > runBenchmark(2e+3)
    Unit: microseconds
                  expr       min        lq      mean    median       uq       max neval
        env_with_list_  8712.146  9138.250 10185.533 10257.678 10761.33 12058.264     5
                    c_ 13407.657 13413.739 13620.976 13605.696 13790.05 13887.738     5
                 list_   854.110   913.407  1064.463   914.167  1301.50  1339.132     5
              by_index 11656.866 11705.140 12182.104 11997.446 12741.70 12809.363     5
               append_ 15986.712 16817.635 17409.391 17458.502 17480.55 19303.560     5
     env_as_container_ 19777.559 20401.702 20589.856 20606.961 20939.56 21223.502     5
    > runBenchmark(2e+4)
    Unit: milliseconds
                  expr         min         lq        mean    median          uq         max neval
        env_with_list_  534.955014  550.57150  550.329366  553.5288  553.955246  558.636313     5
                    c_ 1448.014870 1536.78905 1527.104276 1545.6449 1546.462877 1558.609706     5
                 list_    8.746356    8.79615    9.162577    8.8315    9.601226    9.837655     5
              by_index  953.989076 1038.47864 1037.859367 1064.3942 1065.291678 1067.143200     5
               append_ 1634.151839 1682.94746 1681.948374 1689.7598 1696.198890 1706.683874     5
     env_as_container_  204.134468  205.35348  208.011525  206.4490  208.279580  215.841129     5
    > 
    

    首先,关于 min/lq/mean/median/uq/max 值的一个词:由于我们为 5 次运行中的每一次执行完全相同的任务,在理想的世界中,我们可以预期它需要每次运行的时间完全相同。但是由于我们正在测试的代码还没有加载到 CPU 的缓存中,第一次运行通常偏向于更长的时间。在第一次运行之后,我们希望时间相当一致,但有时我们的代码可能会由于计时器滴答中断或其他与我们正在测试的代码无关的硬件中断而从缓存中逐出。通过对代码 sn-ps 进行 5 次测试,我们允许在第一次运行期间将代码加载到缓存中,然后让每个 sn-p 有 4 次机会在不受外部事件干扰的情况下运行完成。出于这个原因,并且因为我们每次都在完全相同的输入条件下运行完全相同的代码,所以我们将只考虑“分钟”时间就足以在各种代码选项之间进行最佳比较。

    请注意,我选择先运行问题规模为 2000,然后运行 ​​20000,因此我的问题规模从第一次运行到第二次增加了 10 倍。

    list 解决方案的性能:O(1)(恒定时间)

    让我们首先看看list 解决方案的增长情况,因为我们可以立即看出它是两个分析运行中最快的解决方案:在第一次运行中,它花费了 854 秒( 0.854 毫秒秒)执行 2000 个“附加”任务。在第二次运行中,执行 20000 个“追加”任务花费了 8.746 毫秒。一个天真的观察者会说,“啊,list 的解决方案呈现出 O(n) 的增长,因为随着问题规模增长了十倍,执行测试所需的时间也增长了。” 该分析的问题在于 OP 想要的是单个对象插入的增长率,而不是整个问题的增长率。知道了这一点,很明显list 解决方案提供了 OP 想要的东西:一种在 O(1) 时间内将对象附加到列表的方法。

    其他解决方案的性能

    其他解决方案的速度都比不上list 解决方案的速度,但无论如何检查它们是有益的:

    大多数其他解决方案的性能似乎都是 O(n)。例如,by_index 解决方案,一个非常流行的解决方案,基于我在其他 SO 帖子中找到的频率,附加 2000 个对象需要 11.6 毫秒,附加 10 倍对象需要 953 毫秒。整个问题的时间增长了 100 倍,所以一个天真的观察者可能会说 “啊,by_index 解决方案表现出 O(n2) 增长,因为随着问题规模增长十倍,执行测试所需的时间增加了 100 倍。” 和以前一样,这个分析是有缺陷的,因为 OP 对单个对象插入的增长感兴趣。如果我们将整体时间增长除以问题的大小增长,我们发现附加对象的时间增长仅增加了 10 倍,而不是 100 倍,这与问题大小的增长相匹配,所以by_index解决方案是 O(n)。没有列出的解决方案显示附加单个对象的 O(n2) 增长。

    【讨论】:

    • 致读者:请阅读 JanKanis 的回答,它为我上面的发现提供了一个非常实用的扩展,并根据 R 的 C 实现的内部工作原理深入探讨了各种解决方案的开销。
    • 不确定列表选项是否实现了它的要求:>length(c(c(c(list(1)),list(2)),list(3))) [1] 3 > length(list(list(list(list(1)),list(2)),list(3))) [1] 2. 看起来更像嵌套列表。
    • @Picarus - 我认为你是对的。我不再使用 R,但幸运的是 JanKanis 发布了一个更有用的 O(1) 解决方案的答案,并指出了您发现的问题。我相信 JanKanis 会感谢您的支持。
    • @phonetagger,你应该编辑你的答案。不是每个人都会阅读所有答案。
    • "没有一个答案解决了实际的问题" --> 问题是原始问题与算法复杂性无关,请查看问题的版本。 OP 首先询问如何在列表中附加元素,然后几个月后,他改变了问题。
    【解决方案7】:

    在其他答案中,只有 list 方法会导致 O(1) 追加,但它会导致深度嵌套的列表结构,而不是简单的单个列表。我使用了以下数据结构,它们支持 O(1)(摊销)附加,并允许将结果转换回普通列表。

    expandingList <- function(capacity = 10) {
        buffer <- vector('list', capacity)
        length <- 0
    
        methods <- list()
    
        methods$double.size <- function() {
            buffer <<- c(buffer, vector('list', capacity))
            capacity <<- capacity * 2
        }
    
        methods$add <- function(val) {
            if(length == capacity) {
                methods$double.size()
            }
    
            length <<- length + 1
            buffer[[length]] <<- val
        }
    
        methods$as.list <- function() {
            b <- buffer[0:length]
            return(b)
        }
    
        methods
    }
    

    linkedList <- function() {
        head <- list(0)
        length <- 0
    
        methods <- list()
    
        methods$add <- function(val) {
            length <<- length + 1
            head <<- list(head, val)
        }
    
        methods$as.list <- function() {
            b <- vector('list', length)
            h <- head
            for(i in length:1) {
                b[[i]] <- head[[2]]
                head <- head[[1]]
            }
            return(b)
        }
        methods
    }
    

    按如下方式使用它们:

    > l <- expandingList()
    > l$add("hello")
    > l$add("world")
    > l$add(101)
    > l$as.list()
    [[1]]
    [1] "hello"
    
    [[2]]
    [1] "world"
    
    [[3]]
    [1] 101
    

    这些解决方案可以扩展为支持所有列表相关操作的完整对象,但这仍将作为读者的练习。

    命名列表的另一种变体:

    namedExpandingList <- function(capacity = 10) {
        buffer <- vector('list', capacity)
        names <- character(capacity)
        length <- 0
    
        methods <- list()
    
        methods$double.size <- function() {
            buffer <<- c(buffer, vector('list', capacity))
            names <<- c(names, character(capacity))
            capacity <<- capacity * 2
        }
    
        methods$add <- function(name, val) {
            if(length == capacity) {
                methods$double.size()
            }
    
            length <<- length + 1
            buffer[[length]] <<- val
            names[length] <<- name
        }
    
        methods$as.list <- function() {
            b <- buffer[0:length]
            names(b) <- names[0:length]
            return(b)
        }
    
        methods
    }
    

    基准测试

    使用@phonetagger 的代码(基于@Cron Arconis 的代码)进行性能比较。我还添加了better_env_as_container 并稍微更改了env_as_container_。原始的env_as_container_ 已损坏,实际上并未存储所有数字。

    library(microbenchmark)
    lPtrAppend <- function(lstptr, lab, obj) {lstptr[[deparse(lab)]] <- obj}
    ### Store list inside new environment
    envAppendList <- function(lstptr, obj) {lstptr$list[[length(lstptr$list)+1]] <- obj} 
    env2list <- function(env, len) {
        l <- vector('list', len)
        for (i in 1:len) {
            l[[i]] <- env[[as.character(i)]]
        }
        l
    }
    envl2list <- function(env, len) {
        l <- vector('list', len)
        for (i in 1:len) {
            l[[i]] <- env[[paste(as.character(i), 'L', sep='')]]
        }
        l
    }
    runBenchmark <- function(n) {
        microbenchmark(times = 5,  
            env_with_list_ = {
                listptr <- new.env(parent=globalenv())
                listptr$list <- NULL
                for(i in 1:n) {envAppendList(listptr, i)}
                listptr$list
            },
            c_ = {
                a <- list(0)
                for(i in 1:n) {a = c(a, list(i))}
            },
            list_ = {
                a <- list(0)
                for(i in 1:n) {a <- list(a, list(i))}
            },
            by_index = {
                a <- list(0)
                for(i in 1:n) {a[length(a) + 1] <- i}
                a
            },
            append_ = { 
                a <- list(0)    
                for(i in 1:n) {a <- append(a, i)} 
                a
            },
            env_as_container_ = {
                listptr <- new.env(hash=TRUE, parent=globalenv())
                for(i in 1:n) {lPtrAppend(listptr, i, i)} 
                envl2list(listptr, n)
            },
            better_env_as_container = {
                env <- new.env(hash=TRUE, parent=globalenv())
                for(i in 1:n) env[[as.character(i)]] <- i
                env2list(env, n)
            },
            linkedList = {
                a <- linkedList()
                for(i in 1:n) { a$add(i) }
                a$as.list()
            },
            inlineLinkedList = {
                a <- list()
                for(i in 1:n) { a <- list(a, i) }
                b <- vector('list', n)
                head <- a
                for(i in n:1) {
                    b[[i]] <- head[[2]]
                    head <- head[[1]]
                }                
            },
            expandingList = {
                a <- expandingList()
                for(i in 1:n) { a$add(i) }
                a$as.list()
            },
            inlineExpandingList = {
                l <- vector('list', 10)
                cap <- 10
                len <- 0
                for(i in 1:n) {
                    if(len == cap) {
                        l <- c(l, vector('list', cap))
                        cap <- cap*2
                    }
                    len <- len + 1
                    l[[len]] <- i
                }
                l[1:len]
            }
        )
    }
    
    # We need to repeatedly add an element to a list. With normal list concatenation
    # or element setting this would lead to a large number of memory copies and a
    # quadratic runtime. To prevent that, this function implements a bare bones
    # expanding array, in which list appends are (amortized) constant time.
        expandingList <- function(capacity = 10) {
            buffer <- vector('list', capacity)
            length <- 0
    
            methods <- list()
    
            methods$double.size <- function() {
                buffer <<- c(buffer, vector('list', capacity))
                capacity <<- capacity * 2
            }
    
            methods$add <- function(val) {
                if(length == capacity) {
                    methods$double.size()
                }
    
                length <<- length + 1
                buffer[[length]] <<- val
            }
    
            methods$as.list <- function() {
                b <- buffer[0:length]
                return(b)
            }
    
            methods
        }
    
        linkedList <- function() {
            head <- list(0)
            length <- 0
    
            methods <- list()
    
            methods$add <- function(val) {
                length <<- length + 1
                head <<- list(head, val)
            }
    
            methods$as.list <- function() {
                b <- vector('list', length)
                h <- head
                for(i in length:1) {
                    b[[i]] <- head[[2]]
                    head <- head[[1]]
                }
                return(b)
            }
    
            methods
        }
    
    # We need to repeatedly add an element to a list. With normal list concatenation
    # or element setting this would lead to a large number of memory copies and a
    # quadratic runtime. To prevent that, this function implements a bare bones
    # expanding array, in which list appends are (amortized) constant time.
        namedExpandingList <- function(capacity = 10) {
            buffer <- vector('list', capacity)
            names <- character(capacity)
            length <- 0
    
            methods <- list()
    
            methods$double.size <- function() {
                buffer <<- c(buffer, vector('list', capacity))
                names <<- c(names, character(capacity))
                capacity <<- capacity * 2
            }
    
            methods$add <- function(name, val) {
                if(length == capacity) {
                    methods$double.size()
                }
    
                length <<- length + 1
                buffer[[length]] <<- val
                names[length] <<- name
            }
    
            methods$as.list <- function() {
                b <- buffer[0:length]
                names(b) <- names[0:length]
                return(b)
            }
    
            methods
        }
    

    结果:

    > runBenchmark(1000)
    Unit: microseconds
                        expr       min        lq      mean    median        uq       max neval
              env_with_list_  3128.291  3161.675  4466.726  3361.837  3362.885  9318.943     5
                          c_  3308.130  3465.830  6687.985  8578.913  8627.802  9459.252     5
                       list_   329.508   343.615   389.724   370.504   449.494   455.499     5
                    by_index  3076.679  3256.588  5480.571  3395.919  8209.738  9463.931     5
                     append_  4292.321  4562.184  7911.882 10156.957 10202.773 10345.177     5
           env_as_container_ 24471.511 24795.849 25541.103 25486.362 26440.591 26511.200     5
     better_env_as_container  7671.338  7986.597  8118.163  8153.726  8335.659  8443.493     5
                  linkedList  1700.754  1755.439  1829.442  1804.746  1898.752  1987.518     5
            inlineLinkedList  1109.764  1115.352  1163.751  1115.631  1206.843  1271.166     5
               expandingList  1422.440  1439.970  1486.288  1519.728  1524.268  1525.036     5
         inlineExpandingList   942.916   973.366  1002.461  1012.197  1017.784  1066.044     5
    > runBenchmark(10000)
    Unit: milliseconds
                        expr        min         lq       mean     median         uq        max neval
              env_with_list_ 357.760419 360.277117 433.810432 411.144799 479.090688 560.779139     5
                          c_ 685.477809 734.055635 761.689936 745.957553 778.330873 864.627811     5
                       list_   3.257356   3.454166   3.505653   3.524216   3.551454   3.741071     5
                    by_index 445.977967 454.321797 515.453906 483.313516 560.374763 633.281485     5
                     append_ 610.777866 629.547539 681.145751 640.936898 760.570326 763.896124     5
           env_as_container_ 281.025606 290.028380 303.885130 308.594676 314.972570 324.804419     5
     better_env_as_container  83.944855  86.927458  90.098644  91.335853  92.459026  95.826030     5
                  linkedList  19.612576  24.032285  24.229808  25.461429  25.819151  26.223597     5
            inlineLinkedList  11.126970  11.768524  12.216284  12.063529  12.392199  13.730200     5
               expandingList  14.735483  15.854536  15.764204  16.073485  16.075789  16.081726     5
         inlineExpandingList  10.618393  11.179351  13.275107  12.391780  14.747914  17.438096     5
    > runBenchmark(20000)
    Unit: milliseconds
                        expr         min          lq       mean      median          uq         max neval
              env_with_list_ 1723.899913 1915.003237 1921.23955 1938.734718 1951.649113 2076.910767     5
                          c_ 2759.769353 2768.992334 2810.40023 2820.129738 2832.350269 2870.759474     5
                       list_    6.112919    6.399964    6.63974    6.453252    6.910916    7.321647     5
                    by_index 2163.585192 2194.892470 2292.61011 2209.889015 2436.620081 2458.063801     5
                     append_ 2832.504964 2872.559609 2983.17666 2992.634568 3004.625953 3213.558197     5
           env_as_container_  573.386166  588.448990  602.48829  597.645221  610.048314  642.912752     5
     better_env_as_container  154.180531  175.254307  180.26689  177.027204  188.642219  206.230191     5
                  linkedList   38.401105   47.514506   46.61419   47.525192   48.677209   50.952958     5
            inlineLinkedList   25.172429   26.326681   32.33312   34.403442   34.469930   41.293126     5
               expandingList   30.776072   30.970438   34.45491   31.752790   38.062728   40.712542     5
         inlineExpandingList   21.309278   22.709159   24.64656   24.290694   25.764816   29.158849     5
    

    我添加了linkedListexpandingList 以及两者的内联版本。 inlinedLinkedList 基本上是list_ 的副本,但它也将嵌套结构转换回普通列表。除此之外,内联和非内联版本之间的区别在于函数调用的开销。

    expandingListlinkedList 的所有变体都显示 O(1) 附加性能,基准时间随附加项目的数量线性缩放。 linkedListexpandingList 慢,函数调用开销也可见。因此,如果您真的需要尽可能快的速度(并且想要坚持使用 R 代码),请使用 expandingList 的内联版本。

    我还查看了 R 的 C 实现,两种方法都应该是 O(1) 追加任何大小,直到内存用完为止。

    我还更改了env_as_container_,原来的版本会将每个项目存储在索引“i”下,覆盖之前附加的项目。我添加的better_env_as_containerenv_as_container_ 非常相似,但没有deparse 的东西。两者都表现出 O(1) 的性能,但它们的开销比链接/扩展列表大得多。

    内存开销

    在 C R 实现中,每个分配的对象有 4 个字和 2 个整数的开销。 linkedList 方法为每个追加分配一个长度为 2 的列表,在 64 位计算机上,每个追加项总共 (4*8+4+4+2*8=) 56 个字节(不包括内存分配开销,因此可能接近 64 个字节)。 expandingList 方法每个附加项使用一个单词,当向量长度加倍时加上一个副本,因此每个项的总内存使用量高达 16 个字节。由于内存都在一个或两个对象中,因此每个对象的开销是微不足道的。我没有深入研究env 的内存使用情况,但我认为它会更接近linkedList

    【讨论】:

    • 如果列表选项不能解决我们试图解决的问题,保留它有什么意义?
    • @Picarus 我不确定你的意思。为什么我把它放在基准测试中?作为与其他选项的比较。 list_ 选项更快,如果您不需要转换为普通列表(即,如果您将结果用作堆栈),它可能会很有用。
    • @Gabor Csardi 在 stackoverflow.com/a/29482211/264177 的另一个问题中发布了一种将环境转换回列表的更快方法。我也在我的系统上进行了基准测试。它的速度大约是 better_env_as_container 的两倍,但仍然比 linkedList 和 expandList 慢。
    • 深度嵌套 (n=99999) 列表对于某些应用程序似乎是可管理和可容忍的:有人想对 nestoR 进行基准测试吗? (对于我用于nestoR 的environment 东西,我仍然有点菜鸟。)我的瓶颈几乎总是花在编码和进行数据分析上的人力时间,但我很欣赏我在这篇文章中找到的基准。至于内存开销,我不介意我的应用程序每个节点最多大约 1 kB。我坚持使用大型数组等。
    【解决方案8】:

    我对这里提到的方法做了一个小比较。

    n = 1e+4
    library(microbenchmark)
    ### Using environment as a container
    lPtrAppend <- function(lstptr, lab, obj) {lstptr[[deparse(substitute(lab))]] <- obj}
    ### Store list inside new environment
    envAppendList <- function(lstptr, obj) {lstptr$list[[length(lstptr$list)+1]] <- obj} 
    
    microbenchmark(times = 5,  
            env_with_list_ = {
                listptr <- new.env(parent=globalenv())
                listptr$list <- NULL
                for(i in 1:n) {envAppendList(listptr, i)}
                listptr$list
            },
            c_ = {
                a <- list(0)
                for(i in 1:n) {a = c(a, list(i))}
            },
            list_ = {
                a <- list(0)
                for(i in 1:n) {a <- list(a, list(i))}
            },
            by_index = {
                a <- list(0)
                for(i in 1:n) {a[length(a) + 1] <- i}
                a
            },
            append_ = { 
                a <- list(0)    
                for(i in 1:n) {a <- append(a, i)} 
                a
            },
            env_as_container_ = {
                listptr <- new.env(parent=globalenv())
                for(i in 1:n) {lPtrAppend(listptr, i, i)} 
                listptr
            }   
    )
    

    结果:

    Unit: milliseconds
                  expr       min        lq       mean    median        uq       max neval cld
        env_with_list_  188.9023  198.7560  224.57632  223.2520  229.3854  282.5859     5  a 
                    c_ 1275.3424 1869.1064 2022.20984 2191.7745 2283.1199 2491.7060     5   b
                 list_   17.4916   18.1142   22.56752   19.8546   20.8191   36.5581     5  a 
              by_index  445.2970  479.9670  540.20398  576.9037  591.2366  607.6156     5  a 
               append_ 1140.8975 1316.3031 1794.10472 1620.1212 1855.3602 3037.8416     5   b
     env_as_container_  355.9655  360.1738  399.69186  376.8588  391.7945  513.6667     5  a 
    

    【讨论】:

    • 这是很棒的信息:永远不会猜到 list = list 不仅是赢家,而且相差 1 到 2 个数量级!
    • 此比较无效:list_ 未按预期创建整数列表。它将包含列表列表。在每次迭代中,都会创建一个包含 2 个元素的新列表,一个是新整数,另一个是同一列表的先前版本。因为值不会被覆盖,所以通过引用进行简单复制是在内部完成的。这就是为什么它如此之快,但我们根本没有相同的对象。对于所有其他示例,我们有一个长度为 n+1 的列表
    • @DavidBellot 是正确的,它意味着基准级别。您可以在结束时将其展平:)
    【解决方案9】:

    事实上,c() 函数有一个微妙之处。如果你这样做:

    x <- list()
    x <- c(x,2)
    x = c(x,"foo")
    

    你会得到预期的:

    [[1]]
    [1]
    
    [[2]]
    [1] "foo"
    

    但是如果你用x &lt;- c(x, matrix(5,2,2)添加一个矩阵,你的列表将有另外4个值5的元素! 你最好这样做:

    x <- c(x, list(matrix(5,2,2))
    

    它适用于任何其他对象,您将按预期获得:

    [[1]]
    [1]
    
    [[2]]
    [1] "foo"
    
    [[3]]
         [,1] [,2]
    [1,]    5    5
    [2,]    5    5
    

    最后,你的函数变成了:

    push <- function(l, ...) c(l, list(...))
    

    它适用于任何类型的对象。你可以更聪明地去做:

    push_back <- function(l, ...) c(l, list(...))
    push_front <- function(l, ...) c(list(...), l)
    

    【讨论】:

      【解决方案10】:

      试试这个函数 lappend

      lappend <- function (lst, ...){
        lst <- c(lst, list(...))
        return(lst)
      }
      

      以及来自此页面的其他建议Add named vector to a list

      再见。

      【讨论】:

        【解决方案11】:

        在 Lisp 中我们是这样做的:

        > l <- c(1)
        > l <- c(2, l)
        > l <- c(3, l)
        > l <- rev(l)
        > l
        [1] 1 2 3
        

        虽然它是“缺点”,而不仅仅是“c”。如果您需要从一个 empy 列表开始,请使用 l

        【讨论】:

        • 太棒了!所有其他解决方案都会返回一些奇怪的列表。
        • 在 Lisp 中,添加到列表是一个 O(1) 操作,而添加运行在 O(n) 中,@flies。性能增益超过了恢复的需要。在 R 中情况并非如此。甚至在通常最类似于 List 列表的 pairlist 中也不是。
        • @Palec “R 中不是这种情况” - 我不确定您指的是哪个“this”。你是说追加不是O(1),还是不是O(n)?
        • 我是说如果你用 Lisp 编码,你的方法效率会很低,@flies。那句话是为了解释为什么要按原样写答案。在 R 中,这两种方法在性能方面是相当的,AFAIK。但现在我不确定摊销的复杂性。自从我写上一条评论以来就没有碰过 R。
        • 在 R 中,这种方法是 O(n)。 c() 函数将其参数复制到新的向量/列表中并返回。
        【解决方案12】:

        这是一种将项目添加到 R 列表的简单方法:

        # create an empty list:
        small_list = list()
        
        # now put some objects in it:
        small_list$k1 = "v1"
        small_list$k2 = "v2"
        small_list$k3 = 1:10
        
        # retrieve them the same way:
        small_list$k1
        # returns "v1"
        
        # "index" notation works as well:
        small_list["k2"]
        

        或以编程方式:

        kx = paste(LETTERS[1:5], 1:5, sep="")
        vx = runif(5)
        lx = list()
        cn = 1
        
        for (itm in kx) { lx[itm] = vx[cn]; cn = cn + 1 }
        
        print(length(lx))
        # returns 5
        

        【讨论】:

        • 这并不是真正的附加。如果我有 100 个对象并且我想以编程方式将它们附加到列表中怎么办? R 有一个append() 函数,但它实际上是一个连接函数,它只适用于向量。
        • append() 作用于向量和列表,是真正的追加(和concatenate基本一样,所以看不出你的问题是什么)
        • 追加函数应该改变现有对象,而不是创建新对象。真正的追加不会有 O(N^2) 行为。
        【解决方案13】:

        我认为您想要做的是实际上通过引用(指针)传递给函数——创建一个新环境(通过引用传递给函数)并添加列表:

        listptr=new.env(parent=globalenv())
        listptr$list=mylist
        
        #Then the function is modified as:
        lPtrAppend <- function(lstptr, obj) {
            lstptr$list[[length(lstptr$list)+1]] <- obj
        }
        

        现在您只是修改现有列表(而不是创建新列表)

        【讨论】:

        • 这似乎再次具有二次时间复杂度。问题显然是列表/向量调整大小没有按照大多数语言通常实现的方式实现。
        • 是的——看起来最后的附加很慢——可能 b/c 列表是递归的,R 最擅长向量操作而不是循环类型操作。这样做要好得多:
        • system.time(for(i in c(1:10000) mylist[i]=i) (几秒钟),或者更好的是在一个操作中完成所有操作:system.time(mylist =list(1:100000)) (不到一秒),那么使用 for 循环修改该预分配列表也会更快。
        【解决方案14】:
        > LL<-list(1:4)
        
        > LL
        
        [[1]]
        [1] 1 2 3 4
        
        > LL<-list(c(unlist(LL),5:9))
        
        > LL
        
        [[1]]
         [1] 1 2 3 4 5 6 7 8 9
        

        【讨论】:

        • 我认为这不是 OP 正在寻找的那种附加内容。
        • 这不是在列表中附加元素。在这里,您正在增加整数向量的元素,它是列表的唯一元素。该列表只有一个元素,一个整数向量。
        【解决方案15】:

        不知道为什么您认为您的第一种方法行不通。您在 lappend 函数中有一个错误:长度(列表)应该是长度(lst)。这工作正常并返回一个带有附加 obj 的列表。

        【讨论】:

        • 你是绝对正确的。代码中有一个错误,我已经修复了它。我已经测试了我提供的lappend(),它的性能似乎与 c() 和 append() 一样好,所有这些都表现出 O(n^2) 的行为。
        【解决方案16】:

        也许你想要这样的东西?

        > push <- function(l, x) {
           lst <- get(l, parent.frame())
           lst[length(lst)+1] <- x
           assign(l, lst, envir=parent.frame())
         }
        > a <- list(1,2)
        > push('a', 6)
        > a
        [[1]]
        [1] 1
        
        [[2]]
        [1] 2
        
        [[3]]
        [1] 6
        

        这不是一个非常有礼貌的功能(分配给parent.frame() 有点粗鲁),但 IIUYC 正是您所要求的。

        【讨论】:

          【解决方案17】:

          如果您将列表变量作为带引号的字符串传递,您可以从函数中访问它,例如:

          push <- function(l, x) {
            assign(l, append(eval(as.name(l)), x), envir=parent.frame())
          }
          

          所以:

          > a <- list(1,2)
          > a
          [[1]]
          [1] 1
          
          [[2]]
          [1] 2
          
          > push("a", 3)
          > a
          [[1]]
          [1] 1
          
          [[2]]
          [1] 2
          
          [[3]]
          [1] 3
          
          > 
          

          或额外的学分:

          > v <- vector()
          > push("v", 1)
          > v
          [1] 1
          > push("v", 2)
          > v
          [1] 1 2
          > 
          

          【讨论】:

          • 这基本上是我想要的行为,但是它仍然在内部调用 append,导致 O(n^2) 性能。
          猜你喜欢
          • 2018-02-13
          • 2010-10-12
          • 2014-06-28
          • 1970-01-01
          • 1970-01-01
          • 2018-02-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多