Ajouter un objet à une liste dans R en temps constant amorti, O (1)?

245

Si j'ai une liste R mylist, vous pouvez y ajouter un élément objcomme ceci:

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

Mais il y a sûrement un moyen plus compact. Quand j'étais nouveau chez R, j'ai essayé d'écrire lappend()comme ça:

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

mais bien sûr, cela ne fonctionne pas en raison de la sémantique de l'appel par nom de R ( lstest effectivement copié lors de l'appel, donc les modifications apportées à lstne sont pas visibles en dehors de la portée de lappend(). Je sais que vous pouvez effectuer un piratage d'environnement dans une fonction R pour atteindre en dehors de la portée de votre fonction et muter l'environnement d'appel, mais cela ressemble à un gros marteau pour écrire une simple fonction d'ajout.

Quelqu'un peut-il suggérer une meilleure façon de procéder? Points bonus si cela fonctionne pour les vecteurs et les listes.

pseudo
la source
5
R a les caractéristiques de données immuables que l'on trouve souvent dans les langages fonctionnels, je déteste dire cela, mais je pense que vous n'avez qu'à y faire face. Il a ses avantages et ses inconvénients
Dan
Quand vous dites «appel par nom», vous voulez vraiment dire «appel par valeur», n'est-ce pas?
Ken Williams
7
Non, ce n'est certainement pas un appel par valeur, sinon ce ne serait pas un problème. R utilise en fait l'appel par besoin ( en.wikipedia.org/wiki/Evaluation_strategy#Call_by_need ).
Nick
4
Une bonne idée est de pré-allouer votre vecteur / liste: N = 100 mylist = vector ('list', N) for (i in 1: N) {#mylist [[i]] = ...} Evitez de 'grandir' 'objets dans R.
Fernando
J'ai accidentellement trouvé la réponse ici, stackoverflow.com/questions/17046336/… Tellement difficile à mettre en œuvre un algorithme si simple!
KH Kim

Réponses:

255

S'il s'agit d'une liste de chaînes, utilisez simplement la c()fonction:

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> 

Cela fonctionne aussi sur les vecteurs, donc j'obtiens les points bonus?

Edit (2015-Feb-01): Cet article arrive pour son cinquième anniversaire. Certains lecteurs aimables continuent de répéter toute lacune, donc voyez certainement certains des commentaires ci-dessous. Une suggestion pour les listtypes:

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

En général, les types R peuvent rendre difficile d'avoir un et un seul idiome pour tous les types et utilisations.

Dirk Eddelbuettel
la source
19
Cela ne s'ajoute pas ... il concatène. LLaurait toujours deux éléments après C(LL, c="harry")est appelé.
Nick
27
Il suffit de réassigner à LL: LL <- c(LL, c="harry").
Dirk Eddelbuettel
51
Cela ne fonctionne qu'avec des chaînes. Si a, b et c sont des vecteurs entiers, le comportement est complètement différent.
Alexandre Rademaker
8
@Dirk: Vous avez les parens imbriqués différemment de moi. Mon appel à c()a 2 arguments: la liste que j'essaie d'ajouter, à savoir list(a=3, b=c(4, 5)), et l'élément que j'essaie d'ajouter, à savoir c=c(6, 7). Si vous utilisez mon approche, vous verrez que 2 éléments de liste sont ajoutés ( 6et 7, avec des noms c1et c2) au lieu d'un seul vecteur à 2 éléments nommé ccomme cela est clairement prévu!
j_random_hacker
7
Telle est la conclusion mylist <- list(mylist, list(obj))? Si oui, ce serait bien de modifier la réponse
Matthew
96

L'OP (dans la révision mise à jour de la question en avril 2012) souhaite savoir s'il existe un moyen d'ajouter à une liste en temps constant amorti, comme cela peut être fait, par exemple, avec un vector<>conteneur C ++ . Jusqu'à présent, la meilleure réponse (s?) Ne montre que les temps d'exécution relatifs pour diverses solutions étant donné un problème de taille fixe, mais ne traite pas directement de l' efficacité algorithmique des différentes solutions . Les commentaires ci-dessous contiennent de nombreuses réponses sur l'efficacité algorithmique de certaines des solutions, mais dans tous les cas à ce jour (en avril 2015), ils arrivent à une conclusion erronée.

L'efficacité algorithmique capture les caractéristiques de croissance, soit en temps (temps d'exécution) soit en espace (quantité de mémoire consommée) à mesure que la taille d'un problème augmente . L'exécution d'un test de performances pour diverses solutions compte tenu d'un problème de taille fixe ne résout pas le taux de croissance des différentes solutions. L'OP souhaite savoir s'il existe un moyen d'ajouter des objets à une liste R en "temps constant amorti". Qu'est-ce que ça veut dire? Pour expliquer, permettez-moi d'abord de décrire le «temps constant»:

  • Croissance constante ou O (1) :

    Si le temps nécessaire pour effectuer une tâche donnée reste le même que la taille du problème double , alors nous disons que l'algorithme présente un temps constant croissance , ou déclaré en notation "Big O", présente une croissance temporelle O (1). Lorsque l'OP dit temps constant "amorti", il signifie simplement "à long terme" ... c.-à-d., Si l'exécution d'une seule opération prend parfois beaucoup plus de temps que la normale (par exemple, si un tampon préalloué est épuisé et nécessite parfois un redimensionnement à un plus grand taille du tampon), tant que la performance moyenne à long terme est à temps constant, nous l'appellerons toujours O (1).

    A titre de comparaison, je décrirai également le "temps linéaire" et le "temps quadratique":

  • Croissance linéaire ou O (n) :

    Si le temps requis pour effectuer une tâche donnée double comme la taille du problème double , alors nous disons que l'algorithme présente un temps linéaire , ou une croissance O (n) .

  • Croissance quadratique ou O (n 2 ) :

    Si le temps requis pour effectuer une tâche donnée augmente du carré de la taille du problème , nous disons que l'algorithme présente une croissance en temps quadratique , ou O (n 2 ) .

Il existe de nombreuses autres classes d'efficacité d'algorithmes; Je m'en remets à l'article Wikipédia pour une discussion plus approfondie.

Je remercie @CronAcronis pour sa réponse, car je suis nouveau à R et c'était agréable d'avoir un bloc de code entièrement construit pour faire une analyse des performances des différentes solutions présentées sur cette page. J'emprunte son code pour mon analyse, que je reproduis (enveloppé dans une fonction) ci-dessous:

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
        }   
    )
}

Les résultats publiés par @CronAcronis semblent définitivement suggérer que la a <- list(a, list(i))méthode est la plus rapide, au moins pour une taille de problème de 10000, mais les résultats pour une seule taille de problème ne tiennent pas compte de la croissance de la solution. Pour cela, nous devons exécuter au moins deux tests de profilage, avec des tailles de problème différentes:

> 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
> 

Tout d'abord, un mot sur les valeurs min / lq / moyenne / médiane / uq / max: Puisque nous effectuons exactement la même tâche pour chacune des 5 exécutions, dans un monde idéal, nous pourrions nous attendre à ce que cela prenne exactement la même chose quantité de temps pour chaque course. Mais la première exécution est normalement biaisée vers des temps plus longs, car le code que nous testons n'est pas encore chargé dans le cache du CPU. Après la première exécution, nous nous attendons à ce que les temps soient assez cohérents, mais parfois notre code peut être expulsé du cache en raison d'interruptions de temporisation ou d'autres interruptions matérielles qui ne sont pas liées au code que nous testons. En testant les extraits de code 5 fois, nous permettons au code d'être chargé dans le cache lors de la première exécution, puis nous donnons à chaque extrait 4 chances de s'exécuter jusqu'à son terme sans interférence d'événements extérieurs. Pour cette raison,

Notez que j'ai choisi d'exécuter d'abord avec une taille de problème de 2000 puis 20000, donc ma taille de problème a augmenté d'un facteur 10 de la première exécution à la seconde.

Performance de la listsolution: O (1) (temps constant)

Examinons d'abord la croissance de la listsolution, car nous pouvons dire tout de suite que c'est la solution la plus rapide dans les deux exécutions de profilage: lors de la première exécution, il a fallu 854 micro secondes (0,854 milli seconde) pour effectuer 2000 tâches d'ajout. Lors de la deuxième exécution, il a fallu 8,746 millisecondes pour effectuer 20000 tâches "d'ajout". Un observateur naïf dirait: «Ah, la listsolution présente une croissance O (n), car à mesure que la taille du problème augmentait d'un facteur dix, le temps requis pour exécuter le test augmentait également. Le problème avec cette analyse est que ce que l'OP veut, c'est le taux de croissance d' une insertion d'objet unique , pas le taux de croissance du problème global. Sachant cela, il est clair quelist fournit exactement ce que l'OP veut: une méthode pour ajouter des objets à une liste en temps O (1).

Performance des autres solutions

Aucune des autres solutions ne se rapproche même de la vitesse de la listsolution, mais il est tout de même instructif de les examiner:

La plupart des autres solutions semblent être O (n) en termes de performances. Par exemple, la by_indexsolution, une solution très populaire basée sur la fréquence à laquelle je la trouve dans d'autres messages SO, a pris 11,6 millisecondes pour ajouter 2000 objets et 953 millisecondes pour ajouter dix fois plus d'objets. Le temps du problème global a augmenté d'un facteur 100, donc un observateur naïf pourrait dire «Ah, la by_indexsolution présente une croissance O (n 2 ), car à mesure que la taille du problème augmentait d'un facteur dix, le temps nécessaire pour exécuter le test augmentait par un facteur de 100. "Comme précédemment, cette analyse est erronée, car l'OP s'intéresse à la croissance d'une insertion d'objet unique. Si nous divisons la croissance globale du temps par la croissance de la taille du problème, nous constatons que la croissance temporelle des objets ajoutés a augmenté d'un facteur de 10 seulement, et non d'un facteur de 100, ce qui correspond à la croissance de la taille du problème, donc la by_indexsolution est O (n). Il n'y a pas de solutions répertoriées qui présentent une croissance O (n 2 ) pour ajouter un seul objet.

phonetagger
la source
1
Au lecteur: Veuillez lire la réponse de JanKanis, qui fournit une extension très pratique à mes conclusions ci-dessus, et plonge un peu dans les frais généraux de diverses solutions compte tenu du fonctionnement interne de la mise en œuvre C de R.
phonetagger
4
Pas sûr que l'option de liste implémente ce dont elle a besoin:> longueur (c (c (c (liste (1)), liste (2)), liste (3))) [1] 3> longueur (liste (liste (liste (liste))) (liste (1)), liste (2)), liste (3))) [1] 2. Ressemble plus à des listes imbriquées.
Picarus
@Picarus - Je pense que vous avez raison. Je ne travaille plus avec R, mais heureusement, JanKanis a posté une réponse avec une solution O (1) beaucoup plus utile et note le problème que vous avez identifié. Je suis sûr que JanKanis appréciera votre vote positif.
phonetagger
@phonetagger, vous devez modifier votre réponse. Tout le monde ne lira pas toutes les réponses.
Picarus
"pas une seule réponse n'a abordé la question réelle" -> Le problème est que la question d'origine n'était pas sur la complexité de l'algorithme, jetez un œil aux éditions de la question. Le PO a d'abord demandé comment ajouter un élément dans une liste, puis, plusieurs mois plus tard, il a changé la question.
Carlos Cinelli
41

Dans les autres réponses, seule l' listapproche aboutit à O (1), mais elle aboutit à une structure de liste profondément imbriquée, et non à une simple liste unique. J'ai utilisé les infrastructures de données ci-dessous, elles prennent en charge les ajouts O (1) (amortis) et permettent de convertir le résultat en une simple liste.

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
}

et

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
}

Utilisez-les comme suit:

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

[[2]]
[1] "world"

[[3]]
[1] 101

Ces solutions pourraient être développées en objets complets qui prennent en charge toutes les opérations liées aux listes, mais cela restera un exercice pour le lecteur.

Une autre variante pour une liste nommée:

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
}

Repères

Comparaison des performances en utilisant le code de @ phonetagger (qui est basé sur le code de @Cron Arconis). J'ai également ajouté un better_env_as_containeret changé env_as_container_un peu. L'original a env_as_container_été cassé et ne stocke pas réellement tous les numéros.

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
    }

résultat:

> 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

J'ai ajouté linkedListet expandingListet une version en ligne des deux. Il inlinedLinkedLists'agit essentiellement d'une copie de list_, mais il convertit également la structure imbriquée en une liste simple. Au-delà, la différence entre les versions en ligne et non en ligne est due à la surcharge des appels de fonction.

Toutes les variantes expandingListet linkedListaffichent les performances de l'ajout O (1), le temps de référence évoluant linéairement avec le nombre d'éléments ajoutés. linkedListest plus lent que expandingList, et la surcharge d'appel de fonction est également visible. Donc, si vous avez vraiment besoin de toute la vitesse que vous pouvez obtenir (et que vous souhaitez vous en tenir au code R), utilisez une version intégrée de expandingList.

J'ai également jeté un œil à l'implémentation C de R, et les deux approches devraient être O (1) ajouter pour n'importe quelle taille jusqu'à ce que vous manquiez de mémoire.

J'ai également changé env_as_container_, la version originale stockerait chaque élément sous l'index "i", écrasant l'élément précédemment ajouté. Le que better_env_as_containerj'ai ajouté est très similaire env_as_container_mais sans les deparsetrucs. Les deux présentent des performances O (1), mais leur surcharge est un peu plus importante que les listes liées / développées.

Surcharge mémoire

Dans l'implémentation CR, il y a une surcharge de 4 mots et 2 entiers par objet alloué. L' linkedListapproche alloue une liste de longueur deux par ajout, pour un total de (4 * 8 + 4 + 4 + 2 * 8 =) 56 octets par élément ajouté sur les ordinateurs 64 bits (hors surcharge d'allocation de mémoire, donc probablement plus proche de 64 octets). L' expandingListapproche utilise un mot par élément ajouté, plus une copie lors du doublement de la longueur du vecteur, donc une utilisation totale de la mémoire allant jusqu'à 16 octets par élément. Étant donné que la mémoire se trouve dans un ou deux objets, la surcharge par objet est insignifiante. Je n'ai pas examiné en profondeur l' envutilisation de la mémoire, mais je pense que ce sera plus proche linkedList.

JanKanis
la source
quel est l'intérêt de conserver l'option liste si elle ne résout pas le problème que nous essayons de résoudre?
Picarus
1
@Picarus Je ne sais pas ce que tu veux dire. Pourquoi je l'ai gardé dans la référence? En comparaison avec les autres options. L' list_option est plus rapide et pourrait être utile si vous n'avez pas besoin de convertir en liste normale, c'est-à-dire si vous utilisez le résultat comme une pile.
JanKanis
@Gabor Csardi a publié un moyen plus rapide de reconvertir les environnements en listes dans une autre question à stackoverflow.com/a/29482211/264177. J'ai également mesuré cela sur mon système. Il est environ deux fois plus rapide better_env_as_container mais toujours plus lent que LinkedList et expandList.
JanKanis
Les listes profondément imbriquées (n = 99999) semblent gérables et tolérables pour certaines applications: quelqu'un veut-il comparer NestoR ? (Je suis toujours un peu un noob à environmentce que j'ai utilisé pour nestoR.) Mon goulot d'étranglement est presque toujours du temps humain passé à coder et à analyser des données, mais j'apprécie les points de repère que j'ai trouvés sur ce post. En ce qui concerne la surcharge de mémoire, cela ne me dérangerait pas jusqu'à environ un Ko par nœud pour mes applications. Je m'accroche à de grands tableaux, etc.
Ana Nimbus
17

Dans le Lisp, nous l'avons fait de cette façon:

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

bien que ce soit «contre», pas seulement «c». Si vous devez commencer avec une liste empy, utilisez l <- NULL.

Arseny
la source
3
Excellent! Toutes les autres solutions renvoient une liste de listes bizarre.
metakermit
4
En Lisp, l'ajout à une liste est une opération O (1), tandis que l'ajout s'exécute dans O (n), @flies. Le besoin de réversion est compensé par le gain de performances. Ce n'est pas le cas dans R. Pas même dans pairlist, qui ressemble généralement le plus aux listes List.
Palec
@Palec "Ce n'est pas le cas dans R" - je ne sais pas à quoi "vous" faites référence. Voulez-vous dire que l'ajout n'est pas O (1), ou ce n'est pas O (n)?
vole le
1
Je dis que si vous codiez en Lisp, votre approche serait inefficace, @flies. Cette remarque visait à expliquer pourquoi la réponse est écrite telle quelle. Dans R, les deux approches sont au niveau de la performance, AFAIK. Mais maintenant, je ne suis pas sûr de la complexité amortie. Je n'ai pas touché R depuis le moment où mon commentaire précédent a été écrit.
Palec
3
Dans R, cette approche sera O (n). La c()fonction copie ses arguments dans un nouveau vecteur / liste et retourne cela.
JanKanis
6

Vous voulez peut-être quelque chose comme ça?

> 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

Ce n'est pas une fonction très polie (assigner à parent.frame()est un peu grossier) mais IIUYC c'est ce que vous demandez.

Ken Williams
la source
6

J'ai fait une petite comparaison des méthodes mentionnées ici.

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
        }   
)

Résultats:

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 
Cron Merdek
la source
C'est une excellente info: n'aurait jamais imaginé que ce list = listn'était pas seulement le gagnant - mais de 1 à 2 commandes ou de magnitude!
javadba
5

Si vous passez la variable de liste sous forme de chaîne entre guillemets, vous pouvez l'atteindre à partir de la fonction comme:

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

alors:

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

[[2]]
[1] 2

> push("a", 3)
> a
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

> 

ou pour un crédit supplémentaire:

> v <- vector()
> push("v", 1)
> v
[1] 1
> push("v", 2)
> v
[1] 1 2
> 
ayman
la source
1
C'est essentiellement le comportement que je veux, mais il appelle toujours l'ajout en interne, ce qui entraîne des performances O (n ^ 2).
Nick
4

Je ne sais pas pourquoi vous ne pensez pas que votre première méthode ne fonctionnera pas. Vous avez un bogue dans la fonction lappend: la longueur (liste) doit être la longueur (lst). Cela fonctionne bien et retourne une liste avec l'obj ajouté.

Paul
la source
3
Tu as tout à fait raison. Il y avait un bug dans le code et je l'ai corrigé. J'ai testé le lappend()que j'ai fourni et il semble fonctionner aussi bien que c () et append (), qui présentent tous un comportement O (n ^ 2).
Nick
2

Je pense que ce que vous voulez faire est en fait passer par référence (pointeur) à la fonction - créer un nouvel environnement (qui est passé par référence aux fonctions) avec la liste qui lui est ajoutée:

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
}

Maintenant, vous ne faites que modifier la liste existante (pas en créer une nouvelle)

DavidM
la source
1
Cela semble avoir à nouveau une complexité temporelle quadratique. Le problème est évidemment que le redimensionnement liste / vecteur n'est pas implémenté comme il est généralement implémenté dans la plupart des langues.
eold
Oui - on dirait que l'ajout à la fin est très lent - probablement les listes b / c sont récursives, et R est préférable aux opérations vectorielles plutôt qu'aux opérations de type boucle. C'est beaucoup mieux à faire:
DavidM
1
system.time (pour (i dans c (1: 10000) mylist [i] = i) (quelques secondes), ou mieux encore, tout cela en une seule opération: system.time (mylist = list (1: 100000)) (moins d'une seconde), puis la modification de cette liste préallouée avec la boucle for sera également plus rapide.
DavidM
2

Il s'agit d'un moyen simple d'ajouter des éléments à une liste 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"]

Ou par programmation:

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
doug
la source
Ce n'est pas vraiment annexé. Que faire si j'ai 100 objets et que je souhaite les ajouter à une liste par programme? R a une append()fonction, mais c'est vraiment une fonction concaténée et elle ne fonctionne que sur les vecteurs.
Nick
append()fonctionne sur les vecteurs et les listes, et c'est un vrai append (qui est fondamentalement le même que concaténé, donc je ne vois pas quel est votre problème)
hadley
8
Une fonction d'ajout doit muter un objet existant, pas en créer un nouveau. Un vrai ajout n'aurait pas de comportement O (N ^ 2).
Nick
2

en fait il y a une subtilité avec la c()fonction. Si tu fais:

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

vous obtiendrez comme prévu:

[[1]]
[1]

[[2]]
[1] "foo"

mais si vous ajoutez une matrice avec x <- c(x, matrix(5,2,2), votre liste aura encore 4 éléments de valeur 5! Vous feriez mieux de faire:

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

Cela fonctionne pour tout autre objet et vous obtiendrez comme prévu:

[[1]]
[1]

[[2]]
[1] "foo"

[[3]]
     [,1] [,2]
[1,]    5    5
[2,]    5    5

Enfin, votre fonction devient:

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

et cela fonctionne pour tout type d'objet. Vous pouvez être plus intelligent et faire:

push_back <- function(l, ...) c(l, list(...))
push_front <- function(l, ...) c(list(...), l)
David Bellot
la source
1

Il y a aussi list.appenddu rlist( lien vers la documentation )

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

C'est très simple et efficace.

skan
la source
1
ne ressemble pas à R pour moi ... Python?
JD Long
1
J'ai fait un montage et je l'ai essayé: c'est très lent. Mieux vaut utiliser la méthode c()ou list. Les deux sont beaucoup plus rapides.
5
En cherchant le code rlist::list.append(), c'est essentiellement un wrapper autour base::c().
nbenn du
1

Pour la validation, j'ai exécuté le code de référence fourni par @Cron. Il y a une différence majeure (en plus de fonctionner plus rapidement sur le nouveau processeur i7): le by_indexmaintenant fonctionne presque aussi bien que le 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

Pour référence, voici le code de référence copié textuellement de la réponse de @ Cron (juste au cas où il en modifierait le contenu plus tard):

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
        }
)
javadba
la source
0
> 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
Soo Lee
la source
2
Je ne pense pas que ce soit le genre d'appendice que le PO recherchait.
joran
Il ne s'agit pas d'ajouter des éléments dans une liste. Ici, vous augmentez les éléments du vecteur entier, qui est le seul élément de la liste. La liste ne contient qu'un seul élément, un vecteur entier.
Sergio
0

C'est une question très intéressante et j'espère que ma réflexion ci-dessous pourrait y apporter une solution. Cette méthode donne une liste plate sans indexation, mais elle a list et unlist pour éviter les structures d'imbrication. Je ne suis pas sûr de la vitesse car je ne sais pas comment la comparer.

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
xappppp
la source
Ce que je veux ajouter, c'est qu'il donne une liste imbriquée à deux niveaux, mais c'est tout. La façon dont la liste et la liste ne fonctionnent pas n'est pas très claire pour moi, mais c'est le résultat en testant le code
xappppp
-1

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

Nous pouvons donc facilement ajouter l'élément / objet en utilisant le code ci-dessus

saravanan saminathan
la source