Почему последовательность 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 шт):

Автор решения: Harry

Давайте считать. Экспоненциальное увеличение, для простоты - в 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)

Что не так? Откуда у вас умножение? Не для каждого ведь элемента перераспределять память надо :)

→ Ссылка