Aggiungi un oggetto a un elenco in R in tempo costante ammortizzato, O (1)?


245

Se ho un elenco R mylist, puoi aggiungere un elemento objin questo modo:

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

Ma sicuramente c'è un modo più compatto. Quando ero nuovo in R, ho provato a scrivere lappend()così:

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

ma ovviamente non funziona a causa della semantica call-by-name di R ( lstviene effettivamente copiata al momento della chiamata, quindi le modifiche lstnon sono visibili al di fuori dell'ambito di lappend(). So che è possibile eseguire l'hacking ambientale in una funzione R per raggiungere all'esterno ambito della tua funzione e muta l'ambiente chiamante, ma sembra un grosso martello scrivere una semplice funzione append.

Qualcuno può suggerire un modo più bello di farlo? Punti bonus se funziona sia per gli elenchi che per i vettori.


5
R ha le caratteristiche dei dati immutabili che si trovano spesso nei linguaggi funzionali, odio dirlo, ma penso che tu debba solo occupartene. Ha i suoi pro e i suoi contro
Dan

Quando dici "chiama per nome" intendi davvero "chiamata per valore", giusto?
Ken Williams,

7
No, sicuramente non è call-by-value, altrimenti questo non sarebbe un problema. R utilizza effettivamente call-by-need ( en.wikipedia.org/wiki/Evaluation_strategy#Call_by_need ).
Nick

4
Una buona idea è di pre-allocare il tuo vettore / elenco: N = 100 mylist = vettore ('list', N) per (i in 1: N) {#mylist [[i]] = ...} Evita di 'crescere 'oggetti in R.
Fernando,

Ho trovato accidentalmente la risposta qui, stackoverflow.com/questions/17046336/… Così difficile da implementare un algoritmo così semplice!
KH Kim,

Risposte:


255

Se è un elenco di stringhe, basta usare la c()funzione:

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> 

Funziona anche con i vettori, quindi ottengo i punti bonus?

Modifica (2015-Feb-01): questo post è in arrivo per il suo quinto compleanno. Alcuni lettori gentili continuano a ripetere eventuali carenze, quindi vedi anche alcuni dei commenti qui sotto. Un suggerimento per i listtipi:

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

In generale, i tipi R possono rendere difficile avere uno e un solo linguaggio per tutti i tipi e usi.


19
Questo non aggiunge ... concatena. LLavrebbe ancora due elementi dopo che C(LL, c="harry")è stato chiamato.
Nick,

27
Basta riassegnare a LL: LL <- c(LL, c="harry").
Dirk Eddelbuettel,

51
Funziona solo con le stringhe. Se a, bec sono vettori interi, il comportamento è completamente diverso.
Alexandre Rademaker,

8
@Dirk: I genitori sono nidificati in modo diverso da me. La mia chiamata a c()ha 2 argomenti: l'elenco che sto cercando di aggiungere, vale a dire list(a=3, b=c(4, 5)), e l'elemento che sto cercando di aggiungere, vale a dire c=c(6, 7). Se usi il mio approccio, vedrai che vengono aggiunti 2 elementi dell'elenco ( 6e 7, con nomi c1e c2) invece di un singolo vettore a 2 elementi chiamato ccome è chiaramente inteso!
j_random_hacker,

7
Quindi è la conclusione mylist <- list(mylist, list(obj))? Se sì, sarebbe bello modificare la risposta
Matteo,

96

L'OP (nella revisione aggiornata della domanda dell'aprile 2012) è interessato a sapere se esiste un modo per aggiungere un elenco a tempo costante ammortizzato, come può essere fatto, ad esempio, con un vector<>contenitore C ++ . Le migliori risposte qui finora mostrano solo i tempi di esecuzione relativi per varie soluzioni dato un problema di dimensioni fisse, ma non affrontano direttamente nessuna delle algoritmi delle varie soluzioni . I commenti che seguono molte delle risposte discutono dell'efficienza algoritmica di alcune soluzioni, ma in ogni caso ad oggi (ad aprile 2015) giungono a conclusioni errate.

L'efficienza algoritmica cattura le caratteristiche di crescita, sia nel tempo (tempo di esecuzione) sia nello spazio (quantità di memoria consumata) al crescere della dimensione del problema . L'esecuzione di un test delle prestazioni per varie soluzioni a causa di un problema di dimensioni fisse non risolve il tasso di crescita delle varie soluzioni. L'OP è interessato a sapere se esiste un modo per aggiungere oggetti a un elenco R in "tempo costante ammortizzato". Cosa significa? Per spiegare, prima lasciami descrivere "tempo costante":

  • Crescita costante o O (1) :

    Se il tempo necessario per eseguire una determinata attività rimane lo stesso della dimensione del problema raddoppia , allora diciamo che l'algoritmo mostra una crescita temporale costante , o dichiarata nella notazione "Big O", mostra una crescita temporale O (1). Quando l'OP dice tempo costante "ammortizzato", significa semplicemente "a lungo termine" ... vale a dire, se l'esecuzione di una singola operazione richiede occasionalmente molto più tempo del normale (ad esempio se un buffer preallocato è esaurito e occasionalmente richiede il ridimensionamento a un valore maggiore buffer size), fintanto che la performance media a lungo termine è costante, la chiameremo O (1).

    Per confronto, descriverò anche "tempo lineare" e "tempo quadratico":

  • Crescita lineare o O (n) :

    Se il tempo necessario per eseguire un determinato compito raddoppia come la dimensione del problema raddoppia , allora diciamo esposizioni algoritmo lineare tempo , o O (n) crescita.

  • Crescita quadratica o O (n 2 ) :

    Se il tempo necessario per eseguire una determinata attività aumenta del quadrato della dimensione del problema , si dice che l'algoritmo mostra un tempo quadratico o una crescita O (n 2 ) .

Esistono molte altre classi di efficienza degli algoritmi; Rinvio all'articolo di Wikipedia per ulteriori discussioni.

Ringrazio @CronAcronis per la sua risposta, dato che sono nuovo di R ed è stato bello avere un blocco di codice completamente costruito per fare un'analisi delle prestazioni delle varie soluzioni presentate in questa pagina. Sto prendendo in prestito il suo codice per la mia analisi, che duplico (racchiuso in una funzione) di seguito:

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

I risultati pubblicati da @CronAcronis sembrano sicuramente suggerire che il a <- list(a, list(i))metodo è più veloce, almeno per una dimensione del problema di 10000, ma i risultati per una singola dimensione del problema non affrontano la crescita della soluzione. Per questo, dobbiamo eseguire un minimo di due test di profilazione, con diverse dimensioni del problema:

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

Prima di tutto, una parola sui valori min / lq / mean / median / uq / max: dal momento che stiamo eseguendo lo stesso identico compito per ciascuna delle 5 corse, in un mondo ideale, potremmo aspettarci che prenda esattamente lo stesso quantità di tempo per ogni corsa. Ma la prima esecuzione è normalmente orientata verso tempi più lunghi a causa del fatto che il codice che stiamo testando non è ancora caricato nella cache della CPU. Dopo la prima esecuzione, ci aspetteremmo che i tempi siano abbastanza coerenti, ma a volte il nostro codice può essere espulso dalla cache a causa di interruzioni di tick del timer o altri interruzioni hardware non correlate al codice che stiamo testando. Testando i frammenti di codice 5 volte, stiamo permettendo al codice di essere caricato nella cache durante la prima esecuzione e quindi dando a ogni frammento 4 possibilità di esecuzione fino al completamento senza interferenze da eventi esterni. Per questa ragione,

Nota che ho scelto di eseguire prima una dimensione del problema di 2000 e poi 20000, quindi la mia dimensione del problema è aumentata di un fattore 10 dalla prima alla seconda.

Prestazioni della listsoluzione: O (1) (tempo costante)

Diamo prima un'occhiata alla crescita della listsoluzione, dal momento che possiamo dire subito che è la soluzione più veloce in entrambe le serie di profilatura: nella prima serie, ci sono voluti 854 micro secondi (0,854 milioni di secondi) per eseguire 2000 attività "append". Nella seconda esecuzione, sono stati necessari 8.746 millisecondi per eseguire 20000 attività "append". Un osservatore ingenuo direbbe: "Ah, la listsoluzione mostra una crescita di O (n), poiché quando la dimensione del problema cresceva di un fattore dieci, così pure il tempo necessario per eseguire il test". Il problema con quell'analisi è che ciò che l'OP vuole è il tasso di crescita dell'inserzione di un singolo oggetto , non il tasso di crescita del problema complessivo. Sapendo questo, è chiaro quindi chelist La soluzione fornisce esattamente ciò che l'OP vuole: un metodo per aggiungere oggetti a un elenco in O (1) volta.

Prestazioni delle altre soluzioni

Nessuna delle altre soluzioni si avvicina nemmeno alla velocità della listsoluzione, ma è comunque istruttivo esaminarli:

La maggior parte delle altre soluzioni sembrano essere O (n) in termini di prestazioni. Ad esempio, la by_indexsoluzione, una soluzione molto popolare basata sulla frequenza con cui la trovo in altri post SO, ha impiegato 11,6 millisecondi per aggiungere 2000 oggetti e 953 millisecondi per aggiungere dieci volte più oggetti. Il tempo complessivo del problema è cresciuto di un fattore 100, quindi un osservatore ingenuo potrebbe dire "Ah, la by_indexsoluzione mostra una crescita di O (n 2 ), poiché con l'aumentare della dimensione del problema di un fattore dieci, il tempo necessario per eseguire il test è cresciuto di un fattore 100 ".Come prima, questa analisi è errata, poiché l'OP è interessato alla crescita dell'inserzione di un singolo oggetto. Se dividiamo la crescita del tempo complessivo per la crescita della dimensione del problema, scopriamo che la crescita del tempo degli oggetti aggiunti è aumentata di un fattore di solo 10, non di un fattore 100, che corrisponde alla crescita della dimensione del problema, quindi la by_indexsoluzione è O (n). Non ci sono soluzioni elencate che mostrano una crescita O (n 2 ) per l'aggiunta di un singolo oggetto.


1
Al lettore: Per favore leggi la risposta di JanKanis, che fornisce un'estensione molto pratica alle mie scoperte sopra, e si tuffa un po 'nel sovraccarico di varie soluzioni, dato il funzionamento interno dell'implementazione C di R.
Fonagger,

4
Non sono sicuro che l'opzione elenco implementa ciò che è richiesto:> lunghezza (c (c (c (elenco (1)), elenco (2)), elenco (3))) [1] 3> lunghezza (elenco (elenco (elenco (list (1)), list (2)), list (3))) [1] 2. Assomiglia di più agli elenchi nidificati.
Picarus,

@Picarus - Penso che tu abbia ragione. Non sto più lavorando con R, ma per fortuna JanKanis ha pubblicato una risposta con una soluzione O (1) molto più utile e nota il problema che hai identificato. Sono sicuro che JanKanis apprezzerà il tuo voto.
Fonagger

@phonetagger, dovresti modificare la tua risposta. Non tutti leggeranno tutte le risposte.
Picarus

"non una sola risposta ha affrontato la domanda reale" -> Il problema è che la domanda originale non riguardava la complessità dell'algoritmo, dai un'occhiata alle edizioni della domanda. L'OP ha chiesto innanzitutto come aggiungere un elemento in un elenco, poi, diversi mesi dopo, ha cambiato la domanda.
Carlos Cinelli,

41

Nelle altre risposte, solo l' listapproccio si traduce in O (1), ma si traduce in una struttura di elenco profondamente nidificata e non in un singolo elenco. Ho usato le strutture dati seguenti, supportano gli accodamenti O (1) (ammortizzati) e consentono di riconvertire il risultato in un semplice elenco.

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
}

e

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
}

Usali come segue:

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

[[2]]
[1] "world"

[[3]]
[1] 101

Queste soluzioni potrebbero essere espanse in oggetti completi che supportano da sole tutte le operazioni relative all'elenco, ma rimarranno un esercizio per il lettore.

Un'altra variante per un elenco di nomi:

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
}

Punti di riferimenti

Confronto delle prestazioni usando il codice di @ phonagger (che si basa sul codice di @Cron Arconis). Ho anche aggiunto un better_env_as_containere cambiato env_as_container_un po '. L'originale è env_as_container_stato rotto e in realtà non memorizza tutti i numeri.

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
    }

risultato:

> 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

Ho aggiunto linkedListed expandingListe una versione inline di entrambi. L' inlinedLinkedListè fondamentalmente una copia list_, ma converte anche il retro struttura annidata in una lista pianura. Oltre a ciò, la differenza tra le versioni incorporate e non incorporate è dovuta al sovraccarico delle chiamate di funzione.

Tutte le varianti expandingListe linkedListmostrano le prestazioni di aggiunta di O (1), con il tempo di riferimento scalabile in modo lineare con il numero di elementi aggiunti. linkedListè più lento di expandingListe anche l'overhead della chiamata di funzione è visibile. Quindi, se hai davvero bisogno di tutta la velocità che puoi ottenere (e vuoi attenersi al codice R), usa una versione incorporata di expandingList.

Ho anche dato un'occhiata all'implementazione C di R, ed entrambi gli approcci dovrebbero essere O (1) append per qualsiasi dimensione fino a quando non esaurisci la memoria.

Ho anche cambiato env_as_container_, la versione originale memorizza ogni articolo sotto l'indice "i", sovrascrivendo l'articolo precedentemente aggiunto. L' better_env_as_containerho aggiunto è molto simile env_as_container_ma senza deparseroba. Entrambi mostrano prestazioni O (1), ma hanno un overhead leggermente più grande degli elenchi collegati / in espansione.

Sovraccarico di memoria

Nell'implementazione CR c'è un sovraccarico di 4 parole e 2 ints per oggetto allocato. L' linkedListapproccio alloca un elenco di lunghezza due per append, per un totale di (4 * 8 + 4 + 4 + 2 * 8 =) 56 byte per elemento aggiunto su computer a 64 bit (escluso l'overhead di allocazione della memoria, quindi probabilmente più vicino a 64 byte). L' expandingListapproccio utilizza una parola per elemento aggiunto, oltre a una copia quando raddoppia la lunghezza del vettore, quindi un utilizzo totale della memoria fino a 16 byte per elemento. Poiché la memoria è tutta in uno o due oggetti, l'overhead per oggetto è insignificante. Non ho approfondito l' envutilizzo della memoria, ma penso che sarà più vicino linkedList.


che senso ha mantenere la lista se non risolve il problema che stiamo cercando di risolvere?
Picarus

1
@Picarus Non sono sicuro di cosa intendi. Perché l'ho tenuto nel benchmark? In confronto con le altre opzioni. L' list_opzione è più veloce e potrebbe essere utile se non è necessario convertire in un elenco normale, ovvero se si utilizza il risultato come stack.
JanKanis,

@Gabor Csardi ha pubblicato un modo più veloce per convertire gli ambienti in elenchi in una domanda diversa su stackoverflow.com/a/29482211/264177. Ho analizzato anche questo sul mio sistema. È circa due volte più veloce better_env_as_container ma ancora più lento di linkedList e expandingList.
JanKanis,

Gli elenchi profondamente nidificati (n = 99999) sembrano gestibili e tollerabili per alcune applicazioni: qualcuno vuole fare il benchmark di nestoR ? (Sono ancora un po ' environmentindecente per le cose che ho usato per nestoR.) Il mio collo di bottiglia è quasi sempre il tempo umano impiegato a programmare e fare analisi dei dati, ma apprezzo i parametri di riferimento che ho trovato in questo post. Per quanto riguarda l'overhead di memoria, non mi dispiacerebbe fino a circa un KB per nodo per le mie applicazioni. Mi aggrappo a grandi array, ecc.
Ana Nimbus

17

Nel Lisp lo abbiamo fatto in questo modo:

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

sebbene fosse "contro", non solo "c". Se devi iniziare con un elenco empy, usa l <- NULL.


3
Eccellente! Tutte le altre soluzioni restituiscono un elenco strano di elenchi.
Metakermit,

4
In Lisp, anteporre a un elenco è un'operazione O (1), mentre l'aggiunta viene eseguita in O (n), @flies. La necessità di inversione è compensata dall'aumento delle prestazioni. Questo non è il caso di R. Neanche nel pairlist, che generalmente assomiglia di più agli elenchi.
Palec,

@Palec "Questo non è il caso in R" - Non sono sicuro a quale "questo" ti riferisci. Stai dicendo che aggiungere non è O (1) o non è O (n)?
vola il

1
Sto dicendo che se scrivessi in codice Lisp, il tuo approccio sarebbe inefficiente, @flies. Quell'osservazione doveva spiegare perché la risposta è scritta così com'è. In R, i due approcci sono alla pari per quanto riguarda le prestazioni, AFAIK. Ma ora non sono sicuro della complessità ammortizzata. Non ho toccato R da quando è stato scritto il mio commento precedente.
Palec,

3
In R, questo approccio sarà O (n). La c()funzione copia i suoi argomenti in un nuovo vettore / elenco e lo restituisce.
JanKanis,

6

Vuoi qualcosa del genere forse?

> 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

Non è una funzione molto educata (assegnare a parent.frame()è un po 'maleducato) ma IIUYC è quello che stai chiedendo.


6

Ho fatto un piccolo confronto dei metodi citati qui.

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

risultati:

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 

Questa è un'ottima informazione: non avrei mai immaginato che list = listnon fossero solo i vincitori, ma da 1 a 2 ordini o grandezza!
javadba,

5

Se passi la variabile elenco come una stringa tra virgolette, puoi raggiungerla dall'interno della funzione come:

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

così:

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

[[2]]
[1] 2

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

[[2]]
[1] 2

[[3]]
[1] 3

> 

o per credito extra:

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

1
Questo è fondamentalmente il comportamento che desidero, tuttavia chiama ancora append internamente, ottenendo prestazioni O (n ^ 2).
Nick,

4

Non so perché non pensi che il tuo primo metodo non funzionerà. Hai un bug nella funzione lappone: lunghezza (elenco) dovrebbe essere lunghezza (primo). Funziona bene e restituisce un elenco con l'obj allegato.


3
Hai assolutamente ragione. Si è verificato un errore nel codice e l'ho risolto. Ho testato quello lappend()che ho fornito e sembra che si comporti come pure c () e append (), tutti con comportamento O (n ^ 2).
Nick,


2

Penso che ciò che vuoi fare sia in realtà passare per riferimento (puntatore) alla funzione-- creare un nuovo ambiente (che viene passato per riferimento a funzioni) con l'elenco aggiunto ad esso:

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
}

Ora stai solo modificando l'elenco esistente (non crearne uno nuovo)


1
Questo sembra avere di nuovo una complessità temporale quadratica. Il problema è ovviamente che il ridimensionamento elenco / vettoriale non è implementato nel modo in cui viene solitamente implementato nella maggior parte delle lingue.
1111

Sì-- sembra che l'aggiunta alla fine sia molto lenta-- probabilmente gli elenchi b / c sono ricorsivi e R è il migliore nelle operazioni vettoriali piuttosto che nelle operazioni di tipo loop. È molto meglio fare:
DavidM

1
system.time (per (i in c (1: 10000) mylist [i] = i) (pochi secondi), o meglio ancora esegui tutto in un'unica operazione: system.time (mylist = list (1: 100000)) (meno di un secondo), quindi la modifica dell'elenco preallocato con il ciclo for sarà anche più veloce.
DavidM

2

Questo è un modo semplice per aggiungere elementi a un Elenco 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"]

O programmaticamente:

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

Questo non è davvero accodato. Cosa succede se ho 100 oggetti e voglio aggiungerli a un elenco a livello di codice? R ha una append()funzione, ma è davvero una funzione concatenata e funziona solo su vettori.
Nick,

append()funziona su vettori ed elenchi, ed è una vera appendice (che è sostanzialmente la stessa di concatenata, quindi non vedo quale sia il tuo problema)
Hadley

8
Una funzione append dovrebbe mutare un oggetto esistente, non crearne uno nuovo. Un'appendice vera non avrebbe un comportamento O (N ^ 2).
Nick,

2

in effetti esiste una subtelty con la c()funzione. Se fate:

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

otterrai come previsto:

[[1]]
[1]

[[2]]
[1] "foo"

ma se aggiungi una matrice con x <- c(x, matrix(5,2,2), la tua lista avrà altri 4 elementi di valore 5! Faresti meglio a fare:

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

Funziona con qualsiasi altro oggetto e otterrai come previsto:

[[1]]
[1]

[[2]]
[1] "foo"

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

Infine, la tua funzione diventa:

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

e funziona per qualsiasi tipo di oggetto. Puoi essere più intelligente e fare:

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

1

C'è anche list.appenddal rlist( link alla documentazione )

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

È molto semplice ed efficiente.


1
non mi sembra R ... Python?
JD Long

1
Ho fatto una modifica e l'ho provato: è molto lento. Meglio usare il metodo c()o list. Entrambi sono molto più veloci.
5

Cercando un codice per rlist::list.append(), è essenzialmente un wrapper in giro base::c().
nbenn,

1

Per la convalida ho eseguito il codice di riferimento fornito da @Cron. C'è una grande differenza (oltre a funzionare più velocemente sul nuovo processore i7): by_indexora funziona quasi come anche 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

Per riferimento qui è il codice di riferimento copiato alla lettera dalla risposta di @ Cron (nel caso in cui successivamente ne cambi il contenuto):

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

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

2
Non penso che questo sia il tipo di aggiunta che l'OP stava cercando.
joran,

Non si tratta di aggiungere elementi in un elenco. Qui stai aumentando gli elementi del vettore intero, che è l'unico elemento dell'elenco. L'elenco ha solo un elemento, un vettore intero.
Sergio,

0

Questa è una domanda molto interessante e spero che il mio pensiero qui sotto possa contribuire a una soluzione. Questo metodo fornisce un elenco semplice senza indicizzazione, ma ha un elenco e unlist per evitare le strutture di nidificazione. Non sono sicuro della velocità poiché non so come valutarla.

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

Quello che voglio aggiungere è che fornisce un elenco annidato a due livelli, ma questo è tutto. Il modo in cui funziona la lista e la non lista non è molto chiaro per me, ma questo è il risultato testando il codice
xappppp

-1

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

Quindi possiamo facilmente aggiungere l'elemento / oggetto usando il codice sopra

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.