> Какова временная сложность добавления элемента в список Python в конец, начало и середину (Python)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: Сбер
Стек: Python
> Пример ответа
Короткий ответ
Добавление в конец списка - амортизированная O(1), в начало - O(n), в середину - O(n). Это связано с тем, что список хранит элементы в непрерывном массиве: вставка в начало или середину требует сдвига всех последующих элементов. В конец добавление обычно O(1), но при переполнении массива происходит reallocation с копированием всех элементов, поэтому амортизированная сложность остаётся O(1).
Подробное объяснение
Список Python - это динамический массив указателей на объекты. Каждый элемент - это ссылка (8 байт на 64-битной системе), а сами объекты хранятся отдельно в куче.
Добавление в конец (append):
- Если в выделенном массиве есть свободное место - просто записываем указатель по индексу
sizeи увеличиваем размер. Это O(1). - Если массив заполнен - выделяется новый массив большего размера (обычно в 1.125 раза больше), все элементы копируются, затем добавляется новый. Это O(n), но происходит редко. Амортизированная сложность - O(1), потому что количество операций копирования при многократных append'ах в сумме даёт O(n) на n операций.
Добавление в начало (insert(0, x)):
- Все существующие элементы сдвигаются на одну позицию вправо. Это требует O(n) операций копирования указателей, даже если в массиве есть место. Плюс возможный reallocation, но доминирует именно сдвиг.
Добавление в середину (insert(i, x)):
- Сдвигаются элементы с индекса
iдо конца. В среднем это n/2 элементов, поэтому сложность - O(n). Константа меньше, чем для вставки в начало, но асимптотика та же.
Почему не O(1) для начала?
В отличие от связного списка, массив не позволяет вставить элемент без сдвига. Python не использует связный список для list - это осознанный trade-off: быстрый доступ по индексу O(1) ценой медленных вставок в середину.
На практике
- Для частых вставок в начало или середину используйте
collections.deque(для начала/конца) илиbisect.insortс последующей вставкой, если нужен порядок. - Если нужна вставка в середину с сохранением порядка и частыми операциями - рассмотрите
blistилиsortedcontainers.SortedList. - Для массовых вставок в начало эффективнее собрать новый список и развернуть его, чем вызывать
insert(0, ...)в цикле. append- самый быстрый способ расширения списка; для конкатенацииlist1 + list2создаётся новый список - это O(n+m).
Пример кода
PYTHONimport timeit# append - O(1) амортизированноlst = []for i in range(100_000):lst.append(i) # быстро# insert в начало - O(n)lst = list(range(100_000))lst.insert(0, -1) # сдвиг 100k элементов# insert в середину - O(n)lst = list(range(100_000))lst.insert(50_000, -1) # сдвиг ~50k элементов# сравнение времениsetup = "lst = list(range(100_000))"print(timeit.timeit("lst.append(1)", setup=setup, number=1000))print(timeit.timeit("lst.insert(0, 1)", setup=setup, number=1000))print(timeit.timeit("lst.insert(50_000, 1)", setup=setup, number=1000))
Как отвечать на собеседовании
Начните с чёткого ответа: O(1) для конца, O(n) для начала и середины. Затем объясните причину - массив указателей и сдвиг элементов. Упомяните амортизацию для append и почему reallocation не портит асимптотику. Если спросят про deque - скажите, что он реализован как doubly-linked list блоков, поэтому O(1) для обоих концов, но медленный доступ по индексу. Для senior-позиции добавьте про кэш-локальность и почему массив быстрее связного списка на практике.
Что проверяет интервьюер
- Понимание внутреннего устройства
listкак динамического массива. - Знание разницы между амортизированной и худшей сложностью.
- Умение объяснить trade-off между
listиdeque. - Понимание, что сложность вставки зависит от позиции, а не от размера данных (элементы - указатели, копирование ссылок - O(1) на элемент).
- Способность рассуждать о реальной производительности, а не только об асимптотике.
Типичные ошибки
- Утверждение, что
appendвсегда O(1) - забывают про reallocation. - Путаница между
listи связным списком - говорят, что вставка в начало O(1). - Не учитывают, что
insertв середину тоже O(n), а не O(n/2) - константа не влияет на асимптотику. - Предлагают
dequeдля вставки в середину - это ошибка,dequeоптимизирован только для концов. - Забывают, что сложность относится к копированию указателей, а не самих объектов - объекты не перемещаются.
> Похожие задачи по backend
Что такое итераторы и генераторы в Python и в чем их разница
Удалится ли циклическая структура из памяти и когда это происходит в Python?
Что такое range в Python и как он используется в качестве аргумента?
Как выбрать строку с определённым индексом в DataFrame
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью