← Назад к вопросам
Какая сложность операций append, pop, insert для list в Python?
2.0 Middle🔥 151 комментариев
#DevOps и инфраструктура#Django
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Временная сложность операций list в Python
Основные операции и их сложность
| Операция | Сложность | Объяснение |
|---|---|---|
| append(x) | O(1) амортизированная | Добавляет элемент в конец |
| pop() | O(1) | Удаляет последний элемент |
| pop(i) | O(n) | Удаляет элемент в позиции i, требует сдвига |
| insert(i, x) | O(n) | Вставляет перед позицией i, требует сдвига |
| del list[i] | O(n) | То же, что pop(i) |
| remove(x) | O(n) | Удаляет первое вхождение, требует поиска и сдвига |
| index(x) | O(n) | Поиск элемента |
| list[i] | O(1) | Прямой доступ по индексу |
| list[i:j] | O(j-i) | Копирование подсписка |
| extend(iterable) | O(k) | k = длина добавляемого списка |
| reverse() | O(n) | Разворот на месте |
| sort() | O(n log n) | Сортировка Timsort |
Почему append O(1) амортизированная?
Механизм динамического массива в CPython:
# Когда список создан, он выделяет больше памяти, чем нужно
# Это позволяет добавлять элементы без переаллокации каждый раз
lst = [] # capacity = 0, size = 0
lst.append(1) # capacity = 1, size = 1
lst.append(2) # capacity = 2, size = 2 (reallocate)
lst.append(3) # capacity = 4, size = 3 (reallocate, grow by 1.125x)
lst.append(4) # capacity = 4, size = 4
lst.append(5) # capacity = 8, size = 5 (reallocate)
Формула роста: Когда нужна реаллокация, новая ёмкость = old_capacity + (old_capacity >> 3) + 3
Это означает:
- Большую часть времени append работает за O(1)
- Редко происходит реаллокация за O(n)
- В среднем (амортизированная сложность) это O(1)
Экспериментальная проверка
import time
# Проверка append - O(1)
lst = []
start = time.time()
for _ in range(100_000_000):
lst.append(1)
append_time = time.time() - start
print(f"append время: {append_time:.2f}s")
# Проверка insert(0, x) - O(n)
lst = list(range(1_000_000))
start = time.time()
for _ in range(100):
lst.insert(0, 999)
insert_time = time.time() - start
print(f"insert(0, x) время: {insert_time:.2f}s") # Намного медленнее!
# Проверка pop() - O(1)
lst = list(range(10_000_000))
start = time.time()
for _ in range(1_000_000):
lst.pop()
pop_time = time.time() - start
print(f"pop() время: {pop_time:.2f}s")
# Проверка pop(0) - O(n)
lst = list(range(1_000_000))
start = time.time()
for _ in range(100):
lst.pop(0)
pop0_time = time.time() - start
print(f"pop(0) время: {pop0_time:.2f}s") # Медленнее!
Практические последствия
Хорошо (быстро):
# O(1) - append в конец
stack = []
for item in items:
stack.append(item)
value = stack.pop()
Плохо (медленно):
# O(n) для каждой операции - queue на list!
queue = []
for item in items:
queue.append(item)
value = queue.pop(0) # Медленно! O(n) каждый раз
Правильное решение:
from collections import deque
# O(1) для обеих операций
queue = deque()
for item in items:
queue.append(item)
value = queue.popleft() # O(1)!
Когда какая структура нужна?
# Stack (LIFO) - используй list
stack = []
stack.append(value) # O(1)
value = stack.pop() # O(1)
# Queue (FIFO) - используй deque
from collections import deque
queue = deque()
queue.append(value) # O(1)
value = queue.popleft() # O(1)
# Случайный доступ - используй list
arr = [1, 2, 3, 4, 5]
print(arr[2]) # O(1)
# Частые вставки в начало/середину - используй LinkedList
# Но в Python нет встроенного LinkedList
# Используй list только если вставляешь в конец
Внутреннее устройство list
# CPython использует C-массив указателей на объекты Python
# Структура примерно так:
class PyListObject:
ob_item = None # C-массив указателей на объекты
allocated = 0 # Выделенная ёмкость
ob_size = 0 # Текущий размер
# При append:
# 1. Проверяем: ob_size < allocated
# 2. Если да: добавляем элемент за O(1)
# 3. Если нет: выделяем новый больший массив и копируем (O(n))
Ключевые правила для интервью
- append() = O(1) амортизированная, а не O(n)
- pop() = O(1), но pop(i) = O(n)
- insert(0, x) = O(n) - худший случай
- Используй deque для очередей, не list
- list[i:j] копирует подсписок - O(j-i)
- remove(), index() = O(n) - требуют поиска
Трюк: Асимптотическая запись
Можно ответить на вопрос так:
"append() работает за O(1) в среднем благодаря динамическому росту ёмкости. При реаллокации это O(n), но так как число реаллокаций растёт логарифмически, итоговая сложность n операций append остаётся O(n), то есть O(1) в амортизированном смысле".