Додайте об'єкт до списку в R за амортизованим постійним часом, O (1)?


245

Якщо у мене є список R mylist, ви можете додати елемент objдо нього так:

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

Але, безумовно, є якийсь більш компактний спосіб. Коли я був новим в R, я намагався писати lappend()так:

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

але, звичайно, це не працює завдяки семантиці дзвінка за іменем R ( lstефективно копіюється під час виклику, тому зміни lstне видно за lappend()межами. сфера вашої функції та вимкнення середовища виклику, але це здається великим молотком для написання простої функції додавання.

Чи може хтось запропонувати красивіший спосіб зробити це? Бонусні бали, якщо вони працюють як для векторів, так і для списків.


5
R має незмінні характеристики даних, які часто зустрічаються у функціональних мовах, ненавиджу це говорити, але я думаю, що ви просто повинні з цим мати справу. У цього є свої плюси та мінуси
День

Коли ви говорите "call-by-name", ви насправді маєте на увазі "call-by-value", правда?
Кен Вільямс

7
Ні, це точно не є дзвінками за вартістю, інакше це не буде проблемою. R фактично використовує дзвінок за потребою ( en.wikipedia.org/wiki/Evaluation_strategy#Call_by_need ).
Нік

4
Хороша ідея - попередньо виділити вектор / список: N = 100 мій список = вектор ('список', N) для (i в 1: N) {#mylist [[i]] = ...} Уникайте 'зростання 'об’єкти в Р.
Фернандо

Я випадково знайшов відповідь тут, stackoverflow.com/questions/17046336/… Так важко реалізувати такий простий алгоритм!
KH Кім

Відповіді:


255

Якщо це список рядків, просто скористайтеся c()функцією:

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

$b
[1] "dick"

$c
[1] "harry"

R> class(LL)
[1] "list"
R> 

Це працює і на векторах, тому я отримую бонусні бали?

Редагувати (2015-лют-01): Ця публікація виходить у день її п'ятого дня народження. Деякі добрі читачі продовжують повторювати будь-які недоліки з цим, тому обов'язково дивіться деякі коментарі нижче. Одна пропозиція щодо listтипів:

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

Взагалі, типи R можуть ускладнити наявність однієї та лише однієї ідіоми для всіх типів та застосувань.


19
Це не додається ... це об'єднує. LLвсе ще матиме два елементи після того, як C(LL, c="harry")буде викликано.
Нік

27
Просто перепризначити LL: LL <- c(LL, c="harry").
Дірк Еддельбуеттель

51
Це працює лише з рядками. Якщо a, b і c - цілі вектори, поведінка зовсім інша.
Олександр Радемакер

8
@Dirk: Парені укладені інакше, ніж я. У мого заклику c()є два аргументи: список, до якого я намагаюся додати, а саме list(a=3, b=c(4, 5)), і предмет, до якого я намагаюся додати, а саме c=c(6, 7). Якщо ви використовуєте мій підхід, ви побачите, що 2 пункти списку додаються ( 6і 7разом з іменами c1та c2) замість одного двоелементного вектора, названого cтак, як явно призначено!
j_random_hacker

7
Тож висновок mylist <- list(mylist, list(obj))? Якщо так, було б непогано змінити відповідь
Метью

96

ОП (у оновленому перегляді питання у квітні 2012 р.) Зацікавлена ​​в тому, чи існує спосіб додавання до списку за амортизованим постійним часом, як це можна зробити, наприклад, з vector<>контейнером C ++ . Найкраща відповідь (-ів) тут поки що лише показує відносні часи виконання для різних рішень із заданою фіксованою величиною, але не стосується безпосередньо жодного алгоритмічного ефективності різних рішень . У коментарях нижче багато відповідей обговорюють алгоритмічну ефективність деяких рішень, але в кожному випадку на сьогодні (станом на квітень 2015 року) вони приходять до неправильного висновку.

Алгоритмічна ефективність фіксує характеристики росту, як у часі (час виконання), так і в просторі (кількість споживаної пам'яті) у міру збільшення розміру проблеми . Проведення тесту на ефективність для різних рішень із заданою фіксованою величиною не відповідає швидкості росту різних рішень. ОП зацікавлена ​​в тому, щоб знати, чи є спосіб додавання об'єктів до списку R за "амортизований постійний час". Що це означає? Для пояснення спочатку дозвольте мені описати "постійний час":

  • Постійний або O (1) зростання:

    Якщо час, необхідний для виконання заданого завдання, залишається таким самим, як розмір задачі подвоюється , то ми говоримо, що алгоритм демонструє постійний ріст часу , або заявлений у позначенні "Big O", демонструє зростання часу O (1). Коли ОП каже, що "амортизований" постійний час, він просто означає "в довгостроковій перспективі" ... тобто, якщо виконання однієї операції періодично займає набагато більше часу, ніж зазвичай (наприклад, якщо попередньо виділений буфер вичерпується і час від часу вимагає змінити розмір на більший розмір буфера), якщо довгострокова середня продуктивність є постійним часом, ми все одно будемо називати це O (1).

    Для порівняння я також опишу "лінійний час" і "квадратичний час":

  • Лінійний або O (n) зростання:

    Якщо час, необхідний для виконання заданої задачі, подвоюється, коли розмір задачі подвоюється , то ми говоримо, що алгоритм демонструє лінійний час або O (n) зростання.

  • Квадратичне або O (n 2 ) зростання:

    Якщо час, необхідний для виконання заданої задачі, збільшується на квадрат розміру задачі , ми говоримо, що алгоритм демонструє квадратичний час або O (n 2 ) зростання.

Є багато інших класів ефективності алгоритмів; Я відкладаю статтю у Вікіпедії для подальшого обговорення.

Я дякую @CronAcronis за його відповідь, оскільки я новачок у R, і мені було приємно мати повністю сконструйований блок коду для аналізу ефективності різних рішень, представлених на цій сторінці. Я позичаю його код для свого аналізу, який я дублюю (загорнувши у функцію) нижче:

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

Результати, опубліковані @CronAcronis, безумовно, свідчать про те, що a <- list(a, list(i))метод є найшвидшим, принаймні, для розміру проблеми 10000, але результати для одного розміру проблеми не вирішують зростання рішення. Для цього нам потрібно провести як мінімум два тестування профілювання з різними розмірами проблеми:

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

Перш за все, слово про значення min / lq / mean / median / uq / max: Оскільки ми виконуємо абсолютно одне і те ж завдання для кожного з 5 циклів, в ідеальному світі можна було б сподіватися, що це займе саме те саме кількість часу на кожен пробіг. Але звичайно перший запуск упереджений до більш тривалого часу через те, що код, який ми тестуємо, ще не завантажений у кеш процесора. Після першого запуску ми очікуємо, що час буде досить послідовним, але іноді наш код може бути вилучений з кешу через переривання галочок таймера або інших апаратних перерв, не пов'язаних з кодом, який ми тестуємо. Тестуючи фрагменти коду 5 разів, ми дозволяємо завантажувати код у кеш під час першого запуску, а потім надаємо кожному фрагменту 4 шанси запуститись до завершення без втручання з боку подій. З цієї причини,

Зауважте, що я вирішив спершу запустити з розміром проблеми 2000, а потім 20000, тому мій розмір проблеми збільшився в 10 разів від першого запуску до другого.

Продуктивність listрішення: O (1) (постійний час)

Давайте спочатку розглянемо зростання listрішення, оскільки ми можемо одразу сказати, що це найшвидше рішення в обох етапах профілювання: у першому виконанні знадобилося 854 мікросекунди (0,854 мілі секунди), щоб виконати 2000 завдань "додавання". Під час другого запуску знадобилося 8,746 мілісекунд, щоб виконати 20000 завдань "додавання". Наївний спостерігач сказав би: "Ах, listрішення демонструє ріст O (n), оскільки, оскільки розмір проблеми зростав у десять разів, так і час, необхідний для виконання тесту". Проблема цього аналізу полягає в тому, що те, що хоче ОП, - це темп зростання вставки одного об'єкта , а не темп зростання загальної проблеми. Знаючи це, зрозуміло тоді, щоlist рішення забезпечує саме те, що хоче ОП: метод додавання об'єктів до списку за О (1) час.

Продуктивність інших рішень

Жодне з інших рішень не наближається до швидкості listрішення, але все-таки корисно їх вивчити:

Більшість інших рішень, як видається, є O (n) по продуктивності. Наприклад, by_indexрішення, дуже популярне рішення, засноване на частоті, з якою я знаходжу його в інших публікаціях SO, знадобилося 11,6 мілісекунд, щоб додати 2000 об’єктів, і 953 мілісекунди, щоб додати десять разів більше, ніж багато об'єктів. Загальний час проблеми виріс у 100 разів, тому наївний спостерігач міг би сказати: «Ах, by_indexрішення демонструє зростання O (n 2 ), оскільки, оскільки розмір проблеми зростав у десять разів, час, необхідний для виконання тесту, зростав. на коефіцієнт 100. "Як і раніше, цей аналіз є хибним, оскільки ОП зацікавлена ​​у зростанні вставки одного об’єкта. Якщо розділити загальний приріст часу на приріст розміру проблеми, ми виявимо, що приріст часу об'єктів, що додаються, збільшився в 10 разів, а не в 100, що відповідає зростанню розміру задачі, тому by_indexрішення є O (п). Немає перелічених рішень, які демонструють зростання O (n 2 ) для додавання одного об'єкта.


1
Читачеві: Прочитайте відповідь ЯнКаніса, яка забезпечує дуже практичне розширення моїх висновків вище, і трохи занурюється в накладні витрати на різні рішення, враховуючи внутрішню роботу С впровадження Р.
телефонна атака

4
Не впевнений, що параметр списку реалізує те, що потрібно:> довжина (c (c (c (список (1))), список) (1)))), список (2)), список (3))) [1] 3> довжина (список (список (список) (список (1)), список (2)), список (3))) [1] 2. Виглядає більше як вкладені списки.
Пікар

@Picarus - Я думаю, ти маєш рацію. Я більше не працюю з R, але, на щастя, JanKanis опублікував відповідь із набагато кориснішим рішенням O (1) і зазначає проблему, яку ви визначили. Я впевнений, що JanKanis оцінить вашу нагороду.
телефоніст

@phonetagger, ви повинні відредагувати свою відповідь. Не всі прочитають усі відповіді.
Пікар

"жодна відповідь не вирішила актуального питання" -> Проблема полягає в тому, що початкове питання полягало не в складності алгоритму, погляньте на випуски питання. ОП спочатку запитав, як додати елемент до списку, а потім, через кілька місяців, він змінив питання.
Карлос Сінеллі

41

В інших відповідях додається лише listрезультат підходу в O (1), але це призводить до глибоко вкладеної структури списку, а не до простого єдиного списку. Я використав наведені нижче структури даних, вони підтримують додатки O (1) (амортизовані) та дозволяють перетворити результат у звичайний список.

expandingList <- function(capacity = 10) {
    buffer <- vector('list', capacity)
    length <- 0

    methods <- list()

    methods$double.size <- function() {
        buffer <<- c(buffer, vector('list', capacity))
        capacity <<- capacity * 2
    }

    methods$add <- function(val) {
        if(length == capacity) {
            methods$double.size()
        }

        length <<- length + 1
        buffer[[length]] <<- val
    }

    methods$as.list <- function() {
        b <- buffer[0:length]
        return(b)
    }

    methods
}

і

linkedList <- function() {
    head <- list(0)
    length <- 0

    methods <- list()

    methods$add <- function(val) {
        length <<- length + 1
        head <<- list(head, val)
    }

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

Використовуйте їх наступним чином:

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

[[2]]
[1] "world"

[[3]]
[1] 101

Ці рішення можуть бути розширені на повноцінні об'єкти, які підтримують всі операції, пов'язані зі списком, самі, але це залишатиметься вправою для читача.

Ще один варіант для названого списку:

namedExpandingList <- function(capacity = 10) {
    buffer <- vector('list', capacity)
    names <- character(capacity)
    length <- 0

    methods <- list()

    methods$double.size <- function() {
        buffer <<- c(buffer, vector('list', capacity))
        names <<- c(names, character(capacity))
        capacity <<- capacity * 2
    }

    methods$add <- function(name, val) {
        if(length == capacity) {
            methods$double.size()
        }

        length <<- length + 1
        buffer[[length]] <<- val
        names[length] <<- name
    }

    methods$as.list <- function() {
        b <- buffer[0:length]
        names(b) <- names[0:length]
        return(b)
    }

    methods
}

Орієнтири

Порівняння продуктивності за допомогою коду @ phonetagger (який базується на коді @Cron Arconis). Я також додав better_env_as_containerі env_as_container_трохи змінив . Оригінал env_as_container_був зламаний і насправді не зберігає всі номери.

library(microbenchmark)
lPtrAppend <- function(lstptr, lab, obj) {lstptr[[deparse(lab)]] <- obj}
### Store list inside new environment
envAppendList <- function(lstptr, obj) {lstptr$list[[length(lstptr$list)+1]] <- obj} 
env2list <- function(env, len) {
    l <- vector('list', len)
    for (i in 1:len) {
        l[[i]] <- env[[as.character(i)]]
    }
    l
}
envl2list <- function(env, len) {
    l <- vector('list', len)
    for (i in 1:len) {
        l[[i]] <- env[[paste(as.character(i), 'L', sep='')]]
    }
    l
}
runBenchmark <- function(n) {
    microbenchmark(times = 5,  
        env_with_list_ = {
            listptr <- new.env(parent=globalenv())
            listptr$list <- NULL
            for(i in 1:n) {envAppendList(listptr, i)}
            listptr$list
        },
        c_ = {
            a <- list(0)
            for(i in 1:n) {a = c(a, list(i))}
        },
        list_ = {
            a <- list(0)
            for(i in 1:n) {a <- list(a, list(i))}
        },
        by_index = {
            a <- list(0)
            for(i in 1:n) {a[length(a) + 1] <- i}
            a
        },
        append_ = { 
            a <- list(0)    
            for(i in 1:n) {a <- append(a, i)} 
            a
        },
        env_as_container_ = {
            listptr <- new.env(hash=TRUE, parent=globalenv())
            for(i in 1:n) {lPtrAppend(listptr, i, i)} 
            envl2list(listptr, n)
        },
        better_env_as_container = {
            env <- new.env(hash=TRUE, parent=globalenv())
            for(i in 1:n) env[[as.character(i)]] <- i
            env2list(env, n)
        },
        linkedList = {
            a <- linkedList()
            for(i in 1:n) { a$add(i) }
            a$as.list()
        },
        inlineLinkedList = {
            a <- list()
            for(i in 1:n) { a <- list(a, i) }
            b <- vector('list', n)
            head <- a
            for(i in n:1) {
                b[[i]] <- head[[2]]
                head <- head[[1]]
            }                
        },
        expandingList = {
            a <- expandingList()
            for(i in 1:n) { a$add(i) }
            a$as.list()
        },
        inlineExpandingList = {
            l <- vector('list', 10)
            cap <- 10
            len <- 0
            for(i in 1:n) {
                if(len == cap) {
                    l <- c(l, vector('list', cap))
                    cap <- cap*2
                }
                len <- len + 1
                l[[len]] <- i
            }
            l[1:len]
        }
    )
}

# We need to repeatedly add an element to a list. With normal list concatenation
# or element setting this would lead to a large number of memory copies and a
# quadratic runtime. To prevent that, this function implements a bare bones
# expanding array, in which list appends are (amortized) constant time.
    expandingList <- function(capacity = 10) {
        buffer <- vector('list', capacity)
        length <- 0

        methods <- list()

        methods$double.size <- function() {
            buffer <<- c(buffer, vector('list', capacity))
            capacity <<- capacity * 2
        }

        methods$add <- function(val) {
            if(length == capacity) {
                methods$double.size()
            }

            length <<- length + 1
            buffer[[length]] <<- val
        }

        methods$as.list <- function() {
            b <- buffer[0:length]
            return(b)
        }

        methods
    }

    linkedList <- function() {
        head <- list(0)
        length <- 0

        methods <- list()

        methods$add <- function(val) {
            length <<- length + 1
            head <<- list(head, val)
        }

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

        methods
    }

# We need to repeatedly add an element to a list. With normal list concatenation
# or element setting this would lead to a large number of memory copies and a
# quadratic runtime. To prevent that, this function implements a bare bones
# expanding array, in which list appends are (amortized) constant time.
    namedExpandingList <- function(capacity = 10) {
        buffer <- vector('list', capacity)
        names <- character(capacity)
        length <- 0

        methods <- list()

        methods$double.size <- function() {
            buffer <<- c(buffer, vector('list', capacity))
            names <<- c(names, character(capacity))
            capacity <<- capacity * 2
        }

        methods$add <- function(name, val) {
            if(length == capacity) {
                methods$double.size()
            }

            length <<- length + 1
            buffer[[length]] <<- val
            names[length] <<- name
        }

        methods$as.list <- function() {
            b <- buffer[0:length]
            names(b) <- names[0:length]
            return(b)
        }

        methods
    }

результат:

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

Я додав linkedListі expandingListі розкладену версію обох. По inlinedLinkedListсуті, це копія list_, але вона також перетворює вкладену структуру назад у звичайний список. Крім того, різниця між вбудованою та не вкладеною версіями пояснюється накладними витратами на виклики функцій.

Усі варіанти виконання expandingListта linkedListпоказують показник додавання O (1) з орієнтиром масштабування часу лінійно залежно від кількості доданих елементів. linkedListповільніше expandingList, і також видно накладні виклики функцій. Тож якщо вам справді потрібна вся швидкість, яку ви можете отримати (і хочете дотримуватися коду R), використовуйте вбудовану версію expandingList.

Я також ознайомився з реалізацією С на R, і обидва підходи повинні бути додані O (1) для будь-якого розміру до тих пір, поки не закінчиться пам'ять.

Я також змінив env_as_container_, оригінальна версія зберігала б кожен елемент під індексом "i", замінюючи раніше доданий елемент. better_env_as_containerЯ додав дуже схожий , env_as_container_але без deparseречей. Обидва демонструють продуктивність O (1), але вони мають накладні витрати, які є дещо більшими, ніж пов'язані / розширювані списки.

Пам'ять накладні

У реалізації CR є накладні витрати з 4-х слів та 2-х точок на виділений об'єкт. linkedListПідхід виділяє один список довжини два в Append, в цілому (4 * 8 + 4 + 4 + 2 * 8 =) 56 байт на доданому пункту на 64-розрядних комп'ютерах ( за винятком розподілу пам'яті накладних, так що, ймовірно ближче до 64 байт). У expandingListпідході використовується одне слово на доданий елемент плюс копія при подвоєнні довжини вектора, тому загальне використання пам'яті до 16 байт на елемент. Оскільки пам'ять знаходиться в одному або двох об'єктах, накладні витрати на один об'єкт незначні. Я не заглянув глибоко у використання envпам'яті, але думаю, що це буде ближче linkedList.


який сенс зберігати варіант списку, якщо він не вирішує проблему, яку ми намагаємося вирішити?
Пікар

1
@Picarus Я не впевнений, що ти маєш на увазі. Чому я тримав це в еталоні? У порівнянні з іншими варіантами. Цей list_параметр швидший і може бути корисним, якщо вам не потрібно перетворювати у звичайний список, тобто якщо ви використовуєте результат у вигляді стека.
JanKanis

@Gabor Csardi опублікував більш швидкий спосіб перетворення середовищ у списки в іншому запитанні на сайті stackoverflow.com/a/29482211/264177. Я це орієнтував і на мою систему. Це приблизно вдвічі швидше краще_env_as_container, але все ж повільніше, ніж linkedList і розширюється список.
JanKanis

Списки з глибоко вкладеними (n = 99999) видаються керованими та терпимими для певних застосувань: хтось хоче орієнтувати nestoR ? (Я все ще трохи нуб на те, environmentщо я використовував для nestoR.) Моє вузьке місце майже завжди витрачається на кодування людини та аналіз даних, але я ціную орієнтири, які я знайшов у цій публікації. Що стосується накладних витрат на пам'ять, я б не заперечував приблизно до кБ на вузол для своїх додатків. Я тримаюсь на великих масивах тощо
Ана Німбус

17

У Lisp ми це зробили так:

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

хоч це було «мінуси», а не лише «с». Якщо вам потрібно почати зі списку емісій, скористайтеся l <- NULL.


3
Відмінно! Усі інші рішення повертають якийсь дивний список списків.
metakermit

4
У Lisp попередньо додавати до списку - це операція O (1), а додавання виконується в O (n), @flies. Потреба в реверсії переважає за рахунок підвищення продуктивності. Це не так у Р. Навіть у списку пар, який, як правило, найбільше нагадує списки списків.
Палець

@Palec "Це не так у R" - я не впевнений, на яке "це" ви звертаєтесь. Ви говорите, що додавання не є O (1), або це не O (n)?
летить

1
Я кажу, що якби ви кодували в Lisp, ваш підхід був би неефективним, @flies. Це зауваження мало на меті пояснити, чому відповідь написана так, як вона є. У R, два підходи нарівні ефективні, AFAIK. Але зараз я не впевнений у амортизованій складності. Я не торкався R з моменту написання мого попереднього коментаря.
Палець

3
У R цей підхід буде O (n). В c()Функції копіює свої аргументи в новий вектор / список і повертає це.
JanKanis

6

Ви можете щось подібне, можливо?

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

[[2]]
[1] 2

[[3]]
[1] 6

Це не дуже ввічлива функція (присвоєння parent.frame()якоїсь грубій), але IIUYC - це те, про що ви просите.


6

Я зробив невелике порівняння згаданих тут методів.

n = 1e+4
library(microbenchmark)
### Using environment as a container
lPtrAppend <- function(lstptr, lab, obj) {lstptr[[deparse(substitute(lab))]] <- obj}
### Store list inside new environment
envAppendList <- function(lstptr, obj) {lstptr$list[[length(lstptr$list)+1]] <- obj} 

microbenchmark(times = 5,  
        env_with_list_ = {
            listptr <- new.env(parent=globalenv())
            listptr$list <- NULL
            for(i in 1:n) {envAppendList(listptr, i)}
            listptr$list
        },
        c_ = {
            a <- list(0)
            for(i in 1:n) {a = c(a, list(i))}
        },
        list_ = {
            a <- list(0)
            for(i in 1:n) {a <- list(a, list(i))}
        },
        by_index = {
            a <- list(0)
            for(i in 1:n) {a[length(a) + 1] <- i}
            a
        },
        append_ = { 
            a <- list(0)    
            for(i in 1:n) {a <- append(a, i)} 
            a
        },
        env_as_container_ = {
            listptr <- new.env(parent=globalenv())
            for(i in 1:n) {lPtrAppend(listptr, i, i)} 
            listptr
        }   
)

Результати:

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

Це чудова інформація: ніколи б і не здогадувався, що вони list = listбули не тільки переможцем - але на 1–2 порядки чи величину!
javadba

5

Якщо ви переходите до змінної списку у вигляді цитуваного рядка, ви можете дістатись до неї за допомогою функції на зразок:

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

так:

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

[[2]]
[1] 2

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

[[2]]
[1] 2

[[3]]
[1] 3

> 

або за додатковий кредит:

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

1
Це в основному поведінка, яку я хочу, проте вона все ще викликає додавання внутрішньо, що призводить до виконання O (n ^ 2).
Нік

4

Не впевнені, чому ви не вважаєте, що ваш перший метод не працюватиме. У вас є помилка у функції відкидання: length (список) має бути довжиною (lst). Це добре працює і повертає список із доданим obj.


3
Ти абсолютно правий. У коді виявилася помилка, і я її виправив. Я перевірив те, lappend()що я надав, і, схоже, він виконує приблизно так само, як і c () та додавання (), які демонструють поведінку O (n ^ 2).
Нік


2

Я думаю, що ви хочете зробити, це насправді перейти через посилання (покажчик) на функцію - створити нове середовище (яке передається посиланням на функції) із доданим до нього списком:

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
}

Тепер ви лише змінюєте існуючий список (не створюючи новий)


1
Схоже, знову виникає квадратична складність у часі. Очевидно, проблема полягає в тому, що розмір списку / вектора не реалізується так, як це зазвичай реалізується на більшості мов.
eold

Так - схоже, що додавання в кінці дуже повільне - ймовірно, списки b / c є рекурсивними, а R найкраще в векторних операціях, а не в циклі. Це набагато краще зробити:
DavidM

1
system.time (для (i in c (1: 10000) mylist [i] = i) (кілька секунд), а ще краще зробити все за одну операцію: system.time (mylist = список (1: 100000)) (менше секунди), тоді також змінити попередньо виділений список із циклом for також буде швидше.
DavidM

2

Це простий спосіб додати елементи до списку R:

# create an empty list:
small_list = list()

# now put some objects in it:
small_list$k1 = "v1"
small_list$k2 = "v2"
small_list$k3 = 1:10

# retrieve them the same way:
small_list$k1
# returns "v1"

# "index" notation works as well:
small_list["k2"]

Або програмно:

kx = paste(LETTERS[1:5], 1:5, sep="")
vx = runif(5)
lx = list()
cn = 1

for (itm in kx) { lx[itm] = vx[cn]; cn = cn + 1 }

print(length(lx))
# returns 5

Це насправді не додається. Що робити, якщо у мене є 100 об’єктів, і я хочу додавати їх до списку програмно? R має append()функцію, але це дійсно сполучена функція, і вона працює лише на векторах.
Нік

append()працює над векторами та списками, і це справжнє додаток (яке в основному те саме, що об'єднати, тому я не бачу, у чому ваша проблема)
hadley

8
Функція додавання повинна мутувати існуючий об'єкт, а не створювати новий. Справжнє додаток не має поведінки O (N ^ 2).
Нік

2

насправді є підтема з c()функцією. Якщо ти зробиш:

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

Ви отримаєте, як очікувалося:

[[1]]
[1]

[[2]]
[1] "foo"

але якщо ви додасте матрицю з x <- c(x, matrix(5,2,2), ваш список матиме ще 4 елементи значення 5! Вам краще зробити:

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

Він працює для будь-якого іншого об’єкта, і ви отримаєте, як очікувалося:

[[1]]
[1]

[[2]]
[1] "foo"

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

Нарешті, ваша функція стає:

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

і він працює для будь-якого типу об’єктів. Ви можете бути розумнішими і робити:

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

1

Є також list.appendз rlist( посилання на документацію )

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

Це дуже просто і ефективно.


1
не схожий на мене R ... Python?
JD Лонг

1
Я зробив редагування і випробував це: Це страшно повільно. Краще скористайтеся c()або list-методом. І те й інше - швидше.
5

Дивлячись на код для rlist::list.append(), він по суті є обгорткою base::c().
nbenn

1

Для перевірки я запустив контрольний код, наданий @Cron. Є одна основна відмінність (окрім того, що швидше працює на новішому процесорі i7): by_indexтепер працює майже так само, як list_:

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

Для довідки тут наведений контрольний код, скопійований дослівно з відповіді @ Cron (на випадок, якщо він згодом змінює вміст):

n = 1e+4
library(microbenchmark)
### Using environment as a container
lPtrAppend <- function(lstptr, lab, obj) {lstptr[[deparse(substitute(lab))]] <- obj}
### Store list inside new environment
envAppendList <- function(lstptr, obj) {lstptr$list[[length(lstptr$list)+1]] <- obj}

microbenchmark(times = 5,
        env_with_list_ = {
            listptr <- new.env(parent=globalenv())
            listptr$list <- NULL
            for(i in 1:n) {envAppendList(listptr, i)}
            listptr$list
        },
        c_ = {
            a <- list(0)
            for(i in 1:n) {a = c(a, list(i))}
        },
        list_ = {
            a <- list(0)
            for(i in 1:n) {a <- list(a, list(i))}
        },
        by_index = {
            a <- list(0)
            for(i in 1:n) {a[length(a) + 1] <- i}
            a
        },
        append_ = {
            a <- list(0)
            for(i in 1:n) {a <- append(a, i)}
            a
        },
        env_as_container_ = {
            listptr <- new.env(parent=globalenv())
            for(i in 1:n) {lPtrAppend(listptr, i, i)}
            listptr
        }
)

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
Я не думаю, що саме такий варіант додавання ОП шукав.
joran

Це не додані елементи до списку. Тут ви збільшуєте елементи цілого вектора, який є єдиним елементом списку. У списку є лише один елемент, цілий вектор.
Серхіо

0

Це дуже цікаве запитання, і я сподіваюся, що моя думка нижче могла б сприяти його вирішенню. Цей метод дає плоский список без індексації, але він має список і список, щоб уникнути вкладених структур. Я не впевнений у швидкості, оскільки не знаю, як його порівняти.

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

-1

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

Таким чином, ми можемо легко додати елемент / об’єкт за допомогою наведеного вище коду

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.