Почему последовательность append-ов суммируется в O(n), а не в O(n logn)?
static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{
PyObject **items;
size_t new_allocated, num_allocated_bytes;
Py_ssize_t allocated = self->allocated;
/* Bypass realloc() when a previous overallocation is large enough
to accommodate the newsize. If the newsize falls lower than half
the allocated size, then proceed with the realloc() to shrink the list.
*/
if (allocated >= newsize && newsize >= (allocated >> 1)) {
assert(self->ob_item != NULL || newsize == 0);
Py_SIZE(self) = newsize;
return 0;
}
/* This over-allocates proportional to the list size, making room
* for additional growth. The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
* Note: new_allocated won't overflow because the largest possible value
* is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
*/
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
if (new_allocated > (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) {
PyErr_NoMemory();
return -1;
}
if (newsize == 0)
new_allocated = 0;
num_allocated_bytes = new_allocated * sizeof(PyObject *);
items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
if (items == NULL) {
PyErr_NoMemory();
return -1;
}
self->ob_item = items;
Py_SIZE(self) = newsize;
self->allocated = new_allocated;
return 0;
}
Разбирая исходный код метода append
CPython (указан выше), выяснил, что сложность append
равна O(1) при условии, что в массиве есть свободное место, иначе Python увеличивает новый размер экспоненциально (+3/+6 фиксированного количества памяти для списков) и копирует существующие элементы в новый массив. Получается так, что количество перераспределений уменьшается с ростом списка за счет экспоненциального роста выделенного размера массива. Так, например:
lst = []
for i in range(10):
lst.append(i)
На первом перераспределении копируется 1 элемент, на втором - 4, на третьем - 10, и так далее, что в теории дает геометрическую прогрессию вида: 1+4+10+⋯≤O(n). Как я понимаю, c каждой следующей итерацией количество операций между перераспределениями растёт линейно, а общее число перераспределений для n добавлений уменьшается как O(logn). Почему тогда в таком случае для n последовательности append-ов общая сложность равна O(n), а не O(n logn)?
Ответы (1 шт):
Давайте считать. Экспоненциальное увеличение, для простоты - в 2 раза (как вы понимаете, это не принципиально). Т.е. 1, перераспределение - 2, еще перераспределение - 4 и так далее.
Итого для N = 2n (обратите внимание на разницу здесь и далее большой N и маленькой n) элементов перераспределений - n штук. O(log N). Вставок по O(1) тоже N - итого O(N). Теперь о копированиях... Первое - 1 элемент, второе - два, потом 4, последнее - 2n-1.
Но 1+2+4+...+2n-1 = 2n-1 = O(N).
Итого все время для вставки N элементов O(log N)+O(N)+O(N) = O(N)
Что не так? Откуда у вас умножение? Не для каждого ведь элемента перераспределять память надо :)