Hängen Sie ein Objekt in der amortisierten konstanten Zeit an eine Liste in R an. O (1)?


245

Wenn ich eine R-Liste habe mylist, können Sie ein Element objwie folgt anhängen :

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

Aber es gibt sicherlich einen kompakteren Weg. Als ich neu bei R war, habe ich versucht, lappend()so zu schreiben :

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

Aber das funktioniert natürlich nicht aufgrund der Call-by-Name-Semantik von R ( lstwird beim Aufruf effektiv kopiert, sodass Änderungen an lstaußerhalb des Bereichs von nicht sichtbar sind lappend(). Ich weiß, dass Sie in einer R-Funktion Umgebungs-Hacking durchführen können, um außerhalb des Bereichs zu gelangen Umfang Ihrer Funktion und mutieren die aufrufende Umgebung, aber das scheint wie ein großer Hammer, um eine einfache Append-Funktion zu schreiben.

Kann jemand einen schöneren Weg vorschlagen, dies zu tun? Bonuspunkte, wenn es sowohl für Vektoren als auch für Listen funktioniert.


5
R hat die unveränderlichen Dateneigenschaften, die oft in funktionalen Sprachen zu finden sind. Ich hasse es, das zu sagen, aber ich denke, Sie müssen sich nur damit befassen. Es hat seine Vor- und Nachteile
Dan

Wenn Sie "Call-by-Name" sagen, meinen Sie wirklich "Call-by-Value", oder?
Ken Williams

7
Nein, es ist definitiv kein Call-by-Value, sonst wäre dies kein Problem. R verwendet tatsächlich Call-by-Need ( en.wikipedia.org/wiki/Evaluation_strategy#Call_by_need ).
Nick

4
Eine gute Idee ist es, Ihren Vektor / Ihre Liste vorab zuzuweisen: N = 100 mylist = vector ('list', N) für (i in 1: N) {#mylist [[i]] = ...} Vermeiden Sie 'Wachsen' 'Objekte in R.
Fernando

Ich habe hier versehentlich die Antwort gefunden: stackoverflow.com/questions/17046336/… So schwer zu implementieren, so einfacher Algorithmus!
KH Kim

Antworten:


255

Wenn es sich um eine Liste von Zeichenfolgen handelt, verwenden Sie einfach die c()Funktion:

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> 

Das funktioniert auch mit Vektoren. Bekomme ich also die Bonuspunkte?

Bearbeiten (01.02.2015): Dieser Beitrag erscheint an seinem fünften Geburtstag. Einige freundliche Leser wiederholen immer wieder Mängel, daher sehen Sie auf jeden Fall auch einige der folgenden Kommentare. Ein Vorschlag für listTypen:

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

Im Allgemeinen können R-Typen es schwierig machen, eine und nur eine Redewendung für alle Typen und Verwendungen zu haben.


19
Dies hängt nicht an ... es verkettet. LLhätte noch zwei elemente nachdem C(LL, c="harry")aufgerufen wird.
Nick

27
Einfach LL zuordnen : LL <- c(LL, c="harry").
Dirk Eddelbuettel

51
Dies funktioniert nur mit Zeichenfolgen. Wenn a, b und c ganzzahlige Vektoren sind, ist das Verhalten völlig anders.
Alexandre Rademaker

8
@Dirk: Du hast die Parens anders verschachtelt als ich. Mein Aufruf von c()hat zwei Argumente: die Liste, an die ich anhängen möchte list(a=3, b=c(4, 5)), und das Element, an das ich anhängen möchte, nämlich c=c(6, 7). Wenn Sie meinen Ansatz verwenden, werden Sie sehen, dass 2 Listenelemente ( 6und 7mit Namen c1und c2) anstelle eines einzelnen 2-Element-Vektors angehängt werden , der ceindeutig benannt ist!
j_random_hacker

7
So ist die Schlussfolgerung mylist <- list(mylist, list(obj))? Wenn ja, wäre es schön, die Antwort zu ändern
Matthew

96

Das OP (in der im April 2012 aktualisierten Überarbeitung der Frage) ist daran interessiert zu wissen, ob es eine Möglichkeit gibt, eine Liste in amortisierter konstanter Zeit hinzuzufügen, wie dies beispielsweise mit einem C ++ - vector<>Container möglich ist. Die besten Antworten hier zeigen bisher nur die relativen Ausführungszeiten für verschiedene Lösungen bei einem Problem mit fester Größe, gehen jedoch nicht direkt auf die algorithmische Effizienz der verschiedenen Lösungen ein . In den Kommentaren unter vielen Antworten wird die algorithmische Effizienz einiger Lösungen erörtert, aber in jedem bisherigen Fall (Stand April 2015) kommen sie zu dem falschen Ergebnis.

Die algorithmische Effizienz erfasst die Wachstumseigenschaften entweder zeitlich (Ausführungszeit) oder räumlich (Speicherbedarf), wenn die Problemgröße zunimmt . Das Ausführen eines Leistungstests für verschiedene Lösungen bei einem Problem mit fester Größe berücksichtigt nicht die Wachstumsrate der verschiedenen Lösungen. Das OP ist daran interessiert zu wissen, ob es eine Möglichkeit gibt, Objekte in "amortisierter konstanter Zeit" an eine R-Liste anzuhängen. Was bedeutet das? Lassen Sie mich zur Erklärung zunächst "konstante Zeit" beschreiben:

  • Konstantes oder O (1) Wachstum:

    Wenn die zur Ausführung einer bestimmten Aufgabe erforderliche Zeit gleich bleibt, während sich die Größe des Problems verdoppelt , weist der Algorithmus ein konstantes Zeitwachstum auf oder, angegeben in der "Big O" -Notation, ein O (1) -Zeitwachstum. Wenn der OP "amortisierte" konstante Zeit sagt, bedeutet er einfach "auf lange Sicht" ... dh wenn die Ausführung einer einzelnen Operation gelegentlich viel länger als normal dauert (z. B. wenn ein vorab zugewiesener Puffer erschöpft ist und gelegentlich eine Größenänderung auf einen größeren erfordert Puffergröße), solange die langfristige durchschnittliche Leistung eine konstante Zeit ist, nennen wir sie immer noch O (1).

    Zum Vergleich werde ich auch "lineare Zeit" und "quadratische Zeit" beschreiben:

  • Lineares oder O (n) Wachstum:

    Wenn sich die für die Ausführung einer bestimmten Aufgabe erforderliche Zeit verdoppelt, während sich die Größe des Problems verdoppelt , weist der Algorithmus eine lineare Zeit oder ein O (n) -Wachstum auf.

  • Quadratisches oder O (n 2 ) Wachstum:

    Wenn sich die zur Ausführung einer bestimmten Aufgabe erforderliche Zeit um das Quadrat der Problemgröße erhöht , weist der Algorithmus eine quadratische Zeit oder ein O (n 2 ) -Wachstum auf.

Es gibt viele andere Effizienzklassen von Algorithmen. Ich verweise auf den Wikipedia-Artikel zur weiteren Diskussion.

Ich danke @CronAcronis für seine Antwort, da ich neu bei R bin und es schön war, einen vollständig konstruierten Codeblock für eine Leistungsanalyse der verschiedenen auf dieser Seite vorgestellten Lösungen zu haben. Ich leihe seinen Code für meine Analyse aus, den ich unten dupliziere (in eine Funktion eingeschlossen):

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

Die von @CronAcronis veröffentlichten Ergebnisse scheinen definitiv darauf hinzudeuten, dass die a <- list(a, list(i))Methode zumindest für eine Problemgröße von 10000 am schnellsten ist, aber die Ergebnisse für eine einzelne Problemgröße berücksichtigen nicht das Wachstum der Lösung. Dazu müssen mindestens zwei Profiling-Tests mit unterschiedlichen Problemgrößen durchgeführt werden:

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

Zunächst ein Wort zu den Werten min / lq / mean / median / uq / max: Da wir in einer idealen Welt für jeden von 5 Läufen genau die gleiche Aufgabe ausführen, können wir erwarten, dass es genau dasselbe dauert Zeitdauer für jeden Lauf. Der erste Lauf ist jedoch normalerweise auf längere Zeiten ausgerichtet, da der von uns getestete Code noch nicht in den Cache der CPU geladen ist. Nach dem ersten Durchlauf würden wir erwarten, dass die Zeiten ziemlich konsistent sind, aber gelegentlich kann unser Code aufgrund von Timer-Tick-Interrupts oder anderen Hardware-Interrupts, die nicht mit dem von uns getesteten Code zusammenhängen, aus dem Cache entfernt werden. Indem wir die Code-Snippets fünfmal testen, können wir den Code während des ersten Durchlaufs in den Cache laden und dann jedem Snippet 4 Chancen geben, ohne Störung durch externe Ereignisse vollständig ausgeführt zu werden. Deshalb,

Beachten Sie, dass ich mich für die erste Ausführung mit einer Problemgröße von 2000 und dann für 20000 entschieden habe, sodass sich meine Problemgröße vom ersten bis zum zweiten Lauf um den Faktor 10 erhöht hat.

Leistung der listLösung: O (1) (konstante Zeit)

Lassen Sie uns zunächst einen Blick auf das Wachstum der listLösung, da wir sofort sagen kann , dass es die schnellste Lösung in beiden Profilläufe ist: Im ersten Durchgang dauerte es 854 Mikrosekunden (0,854 Milli Sekunden) 2000 „append“ Aufgaben auszuführen. Im zweiten Durchlauf dauerte es 8,746 Millisekunden, um 20000 "Anhängen" -Aufgaben auszuführen. Ein naiver Beobachter würde sagen: "Ah, die listLösung weist ein O (n) -Wachstum auf, da mit zunehmender Problemgröße auch der Zeitaufwand für die Durchführung des Tests zunahm." Das Problem bei dieser Analyse ist, dass das OP die Wachstumsrate einer einzelnen Objekteinfügung wünscht , nicht die Wachstumsrate des Gesamtproblems. Wenn man das weiß, ist es klar, dass dielist Die Lösung bietet genau das, was das OP will: eine Methode zum Anhängen von Objekten an eine Liste in O (1) -Zeit.

Leistung der anderen Lösungen

Keine der anderen Lösungen kommt der Geschwindigkeit der listLösung auch nur annähernd nahe , aber es ist informativ, sie trotzdem zu untersuchen:

Die meisten anderen Lösungen scheinen eine Leistung von O (n) zu haben. Zum Beispiel by_indexdauerte die Lösung, eine sehr beliebte Lösung, basierend auf der Häufigkeit, mit der ich sie in anderen SO-Posts finde, 11,6 Millisekunden, um 2000 Objekte anzuhängen, und 953 Millisekunden, um zehnmal so viele Objekte anzuhängen. Die Zeit des Gesamtproblems wuchs um den Faktor 100, so dass ein naiver Beobachter sagen könnte: "Ah, die by_indexLösung weist ein O (n 2 ) -Wachstum auf, da mit der Zunahme der Problemgröße um den Faktor zehn die für die Durchführung des Tests erforderliche Zeit zunahm um den Faktor 100. "Nach wie vor ist diese Analyse fehlerhaft, da das OP am Wachstum einer einzelnen Objekteinfügung interessiert ist. Wenn wir das gesamte Zeitwachstum durch das Größenwachstum des Problems dividieren, stellen wir fest, dass das Zeitwachstum anhängender Objekte nur um den Faktor 10 und nicht um den Faktor 100 zunimmt, was dem Wachstum der Problemgröße entspricht. Die by_indexLösung lautet also O. (n). Es sind keine Lösungen aufgeführt, die ein O (n 2 ) -Wachstum zum Anhängen eines einzelnen Objekts aufweisen.


1
An den Leser: Bitte lesen Sie die Antwort von JanKanis, die eine sehr praktische Erweiterung meiner obigen Erkenntnisse darstellt, und gehen Sie angesichts der internen Funktionsweise der C-Implementierung von R.
Phonetagger

4
Nicht sicher, ob die Listenoption implementiert, was erforderlich ist:> Länge (c (c (c (Liste (1)), Liste (2)), Liste (3)) [1] 3> Länge (Liste (Liste (Liste) (Liste (1)), Liste (2)), Liste (3)) [1] 2. Sieht eher aus wie verschachtelte Listen.
Picarus

@ Picarus - Ich denke du hast recht. Ich arbeite nicht mehr mit R, aber zum Glück hat JanKanis eine Antwort mit einer viel nützlicheren O (1) -Lösung veröffentlicht und das von Ihnen identifizierte Problem notiert. Ich bin sicher, JanKanis wird Ihre positive Bewertung zu schätzen wissen.
Phonetagger

@phonetagger, du solltest deine Antwort bearbeiten. Nicht jeder wird alle Antworten lesen.
Picarus

"Keine einzige Antwort hat sich mit der eigentlichen Frage befasst" -> Das Problem ist, dass es bei der ursprünglichen Frage nicht um die Komplexität des Algorithmus ging. Schauen Sie sich die Ausgaben der Frage an. Das OP fragte zuerst, wie ein Element in eine Liste eingefügt werden soll, und einige Monate später änderte er die Frage.
Carlos Cinelli

41

In den anderen Antworten wird nur der listAnsatz in O (1) angehängt, aber es entsteht eine tief verschachtelte Listenstruktur und keine einfache einzelne Liste. Ich habe die folgenden Datenstrukturen verwendet, sie unterstützen O (1) (amortisierte) Anhänge und ermöglichen die Rückkonvertierung des Ergebnisses in eine einfache 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
}

und

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
}

Verwenden Sie sie wie folgt:

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

[[2]]
[1] "world"

[[3]]
[1] 101

Diese Lösungen könnten zu vollständigen Objekten erweitert werden, die alle listenbezogenen Vorgänge selbst unterstützen, die jedoch für den Leser eine Übung bleiben.

Eine andere Variante für eine benannte Liste:

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
}

Benchmarks

Leistungsvergleich mit dem Code von @ phonetagger (der auf dem Code von @Cron Arconis basiert). Ich habe auch ein hinzugefügt better_env_as_containerund das env_as_container_ein bisschen geändert . Das Original env_as_container_war kaputt und speichert nicht alle Nummern.

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
    }

Ergebnis:

> 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

Ich habe linkedListund expandingListund eine Inline-Version von beiden hinzugefügt . Das inlinedLinkedListist im Grunde eine Kopie von list_, aber es konvertiert auch die verschachtelte Struktur zurück in eine einfache Liste. Darüber hinaus ist der Unterschied zwischen der Inline- und der Nicht-Inline-Version auf den Overhead der Funktionsaufrufe zurückzuführen.

Alle Varianten von expandingListund linkedListzeigen die Leistung von O (1) -Anhängen, wobei die Benchmark-Zeit linear mit der Anzahl der angehängten Elemente skaliert. linkedListist langsamer als expandingListund der Overhead des Funktionsaufrufs ist ebenfalls sichtbar. Wenn Sie also wirklich die Geschwindigkeit benötigen, die Sie bekommen können (und sich an den R-Code halten möchten), verwenden Sie eine Inline-Version von expandingList.

Ich habe mir auch die C-Implementierung von R angesehen, und beide Ansätze sollten O (1) für jede Größe angehängt werden, bis Ihnen der Speicher ausgeht.

Ich habe auch geändert env_as_container_, die Originalversion würde jedes Element unter dem Index "i" speichern und das zuvor angehängte Element überschreiben. Das, was better_env_as_containerich hinzugefügt habe, ist sehr ähnlich, env_as_container_aber ohne das deparseZeug. Beide weisen eine O (1) -Leistung auf, haben jedoch einen viel größeren Overhead als die verknüpften / expandierenden Listen.

Speicheraufwand

In der CR-Implementierung gibt es einen Overhead von 4 Wörtern und 2 Ints pro zugewiesenem Objekt. Der linkedListAnsatz weist eine Liste mit einer Länge von zwei pro Anhang zu, was insgesamt (4 * 8 + 4 + 4 + 2 * 8 =) 56 Bytes pro angehängtem Element auf 64-Bit-Computern entspricht (ohne Speicherzuweisungsaufwand, also wahrscheinlich näher an 64) Bytes). Der expandingListAnsatz verwendet ein Wort pro angefügtem Element sowie eine Kopie beim Verdoppeln der Vektorlänge, sodass eine Gesamtspeicherauslastung von bis zu 16 Byte pro Element erfolgt. Da sich der Speicher nur in einem oder zwei Objekten befindet, ist der Overhead pro Objekt unbedeutend. Ich habe mich nicht eingehend mit der envSpeichernutzung befasst, aber ich denke, es wird näher daran liegen linkedList.


Was bringt es, die Listenoption beizubehalten, wenn sie das Problem, das wir zu lösen versuchen, nicht löst?
Picarus

1
@ Picarus Ich bin nicht sicher, was du meinst. Warum habe ich es im Benchmark gehalten? Zum Vergleich mit den anderen Optionen. Die list_Option ist schneller und kann nützlich sein, wenn Sie nicht in eine normale Liste konvertieren müssen, dh wenn Sie das Ergebnis als Stapel verwenden.
JanKanis

@Gabor Csardi hat unter stackoverflow.com/a/29482211/264177 eine schnellere Möglichkeit zum Umwandeln von Umgebungen in Listen in einer anderen Frage veröffentlicht. Das habe ich auch auf meinem System gemessen. Es ist ungefähr doppelt so schnell wie better_env_as_container, aber immer noch langsamer als linkedList und expandingList.
JanKanis

Tief verschachtelte (n = 99999) Listen scheinen für bestimmte Anwendungen überschaubar und tolerierbar zu sein: Möchte jemand nestoR vergleichen ? (Ich bin immer noch ein bisschen verrückt nach dem, environmentwas ich für nestoR verwendet habe.) Mein Engpass ist fast immer die menschliche Zeit, die ich mit Codierung und Datenanalyse verbracht habe, aber ich schätze die Benchmarks, die ich in diesem Beitrag gefunden habe. Was den Speicheraufwand angeht, würde es mir nichts ausmachen, bis zu kB pro Knoten für meine Anwendungen zu verwenden. Ich halte mich an großen Arrays usw. fest
Ana Nimbus

17

Im Lisp haben wir es so gemacht:

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

obwohl es "Nachteile" war, nicht nur "c". Wenn Sie mit einer Empy-Liste beginnen müssen, verwenden Sie l <- NULL.


3
Ausgezeichnet! Alle anderen Lösungen geben eine seltsame Liste von Listen zurück.
Metakermit

4
In Lisp ist das Voranstellen einer Liste eine O (1) -Operation, während das Anhängen in O (n), @flies ausgeführt wird. Die Notwendigkeit einer Umkehrung wird durch Leistungssteigerung aufgewogen. Dies ist in R nicht der Fall. Nicht einmal in der Paarliste, die im Allgemeinen den Listenlisten am ähnlichsten ist.
Palec

@Palec "Dies ist in R nicht der Fall" - Ich bin nicht sicher, auf welches "dies" Sie sich beziehen. Wollen Sie damit sagen, dass das Anhängen nicht O (1) oder nicht O (n) ist?
fliegt

1
Ich sage, wenn Sie in Lisp codieren würden, wäre Ihr Ansatz ineffizient, @flies. Diese Bemerkung sollte erklären, warum die Antwort so geschrieben ist, wie sie ist. In R sind die beiden Ansätze in Bezug auf die Leistung AFAIK gleichwertig. Aber jetzt bin ich mir nicht sicher über die amortisierte Komplexität. Ich habe R seit ungefähr dem Zeitpunkt, als mein vorheriger Kommentar geschrieben wurde, nicht mehr berührt.
Palec

3
In R ist dieser Ansatz O (n). Die c()Funktion kopiert ihre Argumente in einen neuen Vektor / eine neue Liste und gibt diese zurück.
JanKanis

6

Willst du so etwas vielleicht?

> 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

Es ist keine sehr höfliche Funktion (Zuweisen parent.frame()ist etwas unhöflich), aber IIUYC ist das, wonach Sie fragen.


6

Ich habe einen kleinen Vergleich der hier genannten Methoden gemacht.

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

Ergebnisse:

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 

Dies ist eine großartige Information: Ich hätte nie gedacht, dass das list = listnicht nur der Gewinner ist - sondern um 1 bis 2 Ordnungen oder Größenordnungen!
Javadba

5

Wenn Sie die Listenvariable als Zeichenfolge in Anführungszeichen übergeben, können Sie sie über die folgende Funktion erreichen:

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

so:

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

[[2]]
[1] 2

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

[[2]]
[1] 2

[[3]]
[1] 3

> 

oder für zusätzliche Gutschrift:

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

1
Dies ist im Grunde das Verhalten, das ich möchte, aber es ruft immer noch intern anhängen auf, was zu einer O (n ^ 2) -Leistung führt.
Nick

4

Ich bin mir nicht sicher, warum Sie nicht glauben, dass Ihre erste Methode nicht funktioniert. Sie haben einen Fehler in der Lappend-Funktion: Länge (Liste) sollte Länge (lst) sein. Dies funktioniert einwandfrei und gibt eine Liste mit dem angehängten Objekt zurück.


3
Du liegst absolut richtig. Es gab einen Fehler im Code und ich habe ihn behoben. Ich habe das getestet lappend(), was ich bereitgestellt habe, und es scheint ungefähr so ​​gut zu funktionieren wie c () und append (), die alle O (n ^ 2) -Verhalten aufweisen.
Nick


2

Ich denke, was Sie tun möchten, ist tatsächlich als Referenz (Zeiger) an die Funktion zu übergeben - erstellen Sie eine neue Umgebung (die als Referenz auf Funktionen übergeben wird) mit der hinzugefügten Liste:

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
}

Jetzt ändern Sie nur die vorhandene Liste (erstellen keine neue)


1
Dies scheint wieder eine quadratische Zeitkomplexität zu haben. Das Problem ist offensichtlich, dass die Größenänderung von Listen / Vektoren nicht so implementiert ist, wie es normalerweise in den meisten Sprachen implementiert ist.
Eold

Ja - es sieht so aus, als ob das Anhängen am Ende sehr langsam ist - wahrscheinlich sind B / C-Listen rekursiv, und R eignet sich am besten für Vektoroperationen und nicht für Schleifenoperationen. Es ist viel besser zu tun:
DavidM

1
system.time (für (i in c (1: 10000) mylist [i] = i) (einige Sekunden) oder besser noch alles in einer Operation: system.time (mylist = list (1: 100000)) (weniger als eine Sekunde), dann wird das Ändern dieser vorab zugewiesenen Liste mit der for-Schleife auch schneller sein.
DavidM

2

Dies ist eine einfache Möglichkeit, Elemente zu einer R-Liste hinzuzufügen:

# 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"]

Oder programmatisch:

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

Dies ist nicht wirklich anhängend. Was ist, wenn ich 100 Objekte habe und diese programmgesteuert an eine Liste anhängen möchte? R hat eine append()Funktion, aber es ist wirklich eine verkettete Funktion und es funktioniert nur mit Vektoren.
Nick

append()arbeitet an Vektoren und Listen, und es ist ein echtes Anhängen (was im Grunde das gleiche ist wie verketten, also sehe ich nicht, was Ihr Problem ist)
Hadley

8
Eine Append-Funktion sollte ein vorhandenes Objekt mutieren und kein neues erstellen. Ein echtes Anhängen hätte kein O (N ^ 2) -Verhalten.
Nick

2

in der Tat gibt es eine Subtilität mit der c()Funktion. Wenn Sie tun:

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

Sie erhalten wie erwartet:

[[1]]
[1]

[[2]]
[1] "foo"

Wenn Sie jedoch eine Matrix mit hinzufügen x <- c(x, matrix(5,2,2), enthält Ihre Liste weitere 4 Wertelemente 5! Du solltest es besser machen:

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

Es funktioniert für jedes andere Objekt und Sie erhalten wie erwartet:

[[1]]
[1]

[[2]]
[1] "foo"

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

Schließlich wird Ihre Funktion:

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

und es funktioniert für jede Art von Objekt. Sie können schlauer sein und Folgendes tun:

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

1

Es gibt auch list.appendaus dem rlist( Link zur Dokumentation )

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

Es ist sehr einfach und effizient.


1
sieht für mich nicht nach R aus ... Python?
JD Long

1
Ich habe einen Schnitt gemacht und ihn ausprobiert: Es ist verdammt langsam. Verwenden Sie besser die c()oder list-Methode. Beide sind viel schneller.
5.

Wenn man nach dem Code sucht rlist::list.append(), ist es im Wesentlichen ein Wrapper base::c().
nbenn

1

Zur Validierung habe ich den von @Cron bereitgestellten Benchmark-Code ausgeführt. Es gibt einen großen Unterschied (zusätzlich zur schnelleren Ausführung auf dem neueren i7-Prozessor): Der by_indexjetzt funktioniert fast genauso gut wie der 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

Als Referenz dient hier der Benchmark-Code, der wörtlich aus der Antwort von @ Cron kopiert wurde (nur für den Fall, dass er später den Inhalt ändert):

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
Ich glaube nicht, dass dies die Art von Anhang ist, nach der das OP gesucht hat.
Joran

Hiermit werden keine Elemente in einer Liste angehängt. Hier erhöhen Sie die Elemente des Ganzzahlvektors, der das einzige Element der Liste ist. Die Liste enthält nur ein Element, einen ganzzahligen Vektor.
Sergio

0

Dies ist eine sehr interessante Frage, und ich hoffe, dass mein Gedanke unten einen Lösungsweg dazu beitragen kann. Diese Methode gibt eine flache Liste ohne Indizierung an, verfügt jedoch über eine Liste und eine Auflistung, um die Verschachtelungsstrukturen zu vermeiden. Ich bin mir über die Geschwindigkeit nicht sicher, da ich nicht weiß, wie ich sie messen soll.

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

Was ich hinzufügen möchte, ist, dass es eine verschachtelte Liste mit zwei Ebenen gibt, aber das war's. Die Art und Weise, wie Liste und Auflistung funktionieren, ist mir nicht sehr klar, aber dies ist das Ergebnis durch Testen des Codes
xappppp

-1

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

So können wir das Element / Objekt mit dem obigen Code einfach anhängen

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.