ОП (у оновленому перегляді питання у квітні 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 ) для додавання одного об'єкта.