Сложность операций: O-нотация, посчитанная на встроенных типах
«Оцените сложность этого кода» — вопрос, на который отвечают буквой O, а проверяют счётом. Прогон считает сравнения напрямую: x in по списку из ста тысяч элементов сделал сто тысяч сравнений, а по множеству без коллизий хешей — одно, с одним хешем на всех — порядка n; bisect — семнадцать; на сто тысяч append список перевыделил память 66 раз; генератор списка с x in по другому списку на тысяче элементов сделал миллион сравнений; а sorted() на уже отсортированных данных — ровно n − 1.
Полное техническое изложение
TL;DR
O-нотация описывает не скорость, а то, как растёт работа с ростом входа. O(1) — работа не зависит от n, O(n) — растёт вместе с n, O(log n) — прибавляет понемногу при каждом удвоении, O(n²) — растёт как квадрат. Строго O — верхняя оценка («не быстрее, чем»), тесную оценку пишут Θ. Работу удобнее всего считать сравнениями: прогон считает их напрямую.
Отсюда главное: оценивают не строку кода, а то, что она делает внутри.
x in list — перебор: на ста тысячах элементов сто тысяч сравнений.
x in set — O(1) в среднем: в нашем наборе без коллизий хешей это одно
сравнение на всех проверенных n, но если у всех элементов один хеш, поиск
среди ста элементов вызвал __eq__ 105 раз — худший случай O(n). Поэтому
безобидный генератор списка
[x for x in a if x in b] со списком b — это O(n²): на тысяче элементов он
сделал миллион сравнений, а с множеством b — ни одного.
Дальше — то, что отличает понимание от пересказа. append —
«амортизированное» O(1): на сто тысяч добавлений список перевыделил память
66 раз, потому что растёт с запасом. Двоичный поиск bisect на ста тысячах —
17 сравнений — но это позиция вставки, «найден ли» решает ещё одна проверка.
А sorted() — O(n log n) в среднем и худшем случае, но на уже
отсортированных данных в нашем прогоне сделал ровно n − 1 сравнений:
сортировка CPython ищет готовые упорядоченные участки.
- что такое список, множество и цикл
for; - что
x in yпроверяет вхождение; - школьное понятие логарифма: log2(1024) = 10;
- класс с методами
__eq__,__lt__и__hash__— урок «Классы и ООП»: прогон считает сравнения через такие методы; - что такое хеш и почему множество ищет по нему — урок «Встроенные коллекции».
- O(1), O(n), O(log n), O(n log n), O(n²) и как их оценивают;
- амортизированное O(1) у
append,bisect; - скрытый квадрат в однострочнике, сложность
sorted().
База: что говорит O-нотация
| запись | как растёт работа | пример во встроенных типах | какой случай |
|---|---|---|---|
| O(1) | не зависит от n | x in set, d[key] | средний; худший — O(n) |
| O(1) | не зависит от n | lst.append(x) | амортизированный; отдельный вызов — до O(n) |
| O(log n) | +1 шаг при удвоении n | bisect по отсортированному списку | худший |
| O(n) | вместе с n | x in list, lst.insert(0, x), min(lst) | худший: у x in list — элемент последний или его нет; у min — любой |
| O(n log n) | чуть быстрее линейного | sorted(lst) | средний и худший; лучший — O(n) |
| O(n²) | как квадрат n | цикл в цикле по одним и тем же данным | — |
Здесь n — число элементов коллекции, а работа — число сравнений элементов
(у append и insert — копирований ссылок), а не время.
Ответ уровня Junior на «что такое O(n)»: это то, как растёт время работы алгоритма с ростом размера входа; константы и младшие слагаемые отбрасывают. Строже: O(f(n)) — асимптотическая верхняя оценка в выбранной модели стоимости: начиная с некоторого n работа не больше c·f(n). Поэтому линейная функция формально и O(n), и O(n²); тесную оценку — растёт ровно как f(n) с точностью до множителя — пишут Θ(f(n)), и на собеседовании под «O(n)» обычно имеют в виду именно её. Это верно, и за этим ответом обычно идут уточнения: какая сложность у конкретных операций списка и множества, что значит «амортизированное» и где квадрат прячется в коде, который выглядит линейным. Механизмы ниже отвечают на них счётом.
Как устроена проверка: элементы — объекты, которые считают вызовы своих
__eq__ и __lt__. Для перебора, двоичного поиска и сортировки число
сравнений и есть мера работы. От скорости машины оно не зависит и совпало
на CPython 3.11–3.14, но это наблюдение реализации для этих объектов, а не
контракт языка: другие данные — например, совпадающие хеши — дают другой
счёт. Вычисление хеша в счёт не входит, и одно сравнение — не одна машинная
операция: у длинных строк и hash, и == проходят по символам (раздел
«Глубже: цена одного сравнения»).
Механизм 1: поиск — O(n), O(1) и O(log n) в числах
Вывод bench/complexity/basics.py, раздел 1:
1) поиск: сколько сравнений, искомый элемент последний
n x in list x in set bisect log2(n)
10 10 1 3 3.3
100 100 1 7 6.6
1000 1000 1 10 10.0
10000 10000 1 13 13.3
100000 100000 1 17 16.6
bisect на неотсортированном [5, 1, 4, 2, 3], ищем 1: индекс 0, а 1 стоит на месте 1 — ошибки нет
Три столбца — три вида роста:
x in list— O(n). Перебор сравнивает элементы по очереди, и сравнений ровно n, когда искомый последний.x in set— O(1) в среднем. Множество вычисляет хеш, идёт в нужную ячейку и сравнивает элемент с теми, у кого тот же хеш. УKeyиз прогона хеши разных значений не совпадают, поэтому__eq__вызвался один раз — при десяти элементах и при ста тысячах. Это наблюдение для этого набора, а не свойство множества: ниже — тот же поиск, когда хеш у всех один.bisect— O(log n). Двоичный поиск по отсортированному списку каждым сравнением отбрасывает половину. n выросло в десять тысяч раз, а сравнений стало на 14 больше — столбец идёт вровень с log2(n).
Если хеш у всех элементов один — вывод bench/complexity/basics.py,
раздел 6:
6) множество, у всех элементов один и тот же хеш: вызовов __eq__
n обычные хеши один хеш: поиск один хеш: построение
10 1 10 45
100 1 105 5982
1000 1 1336 605201
10000 1 11672 57720235
тот же опыт с хешем 0 вместо 1, n = 100: поиск — 114
обычные хеши, искомый — тот же объект, а не равный ему: поиск — 0
С одним хешем на всех поиск последнего вызвал __eq__ 105 раз на ста
элементах и 11 672 на десяти тысячах — порядка n: это худший случай
множества, O(n). Вызовов даже больше n — значит, часть элементов множество
сравнило повторно; точное число зависит от самого значения хеша: с нулём
вместо единицы — 114. Построение такого множества стоит порядка n²/2
сравнений. А элемент, найденный по тождеству — тот же объект, а не равный
ему, — __eq__ не вызывает вовсе. Поэтому «O(1) в среднем» не значит «одно
сравнение», а сами счётчики __eq__ — наблюдение CPython, а не обещание
языка.
bisect — модуль стандартной библиотеки, и его условие записано прямо в
строке документации bisect_left:
return the index where to insert item x in list a, assuming a is sorted
(вернуть индекс, на который надо вставить x в список a, считая, что a отсортирован).
Отсортированность — обязанность того, кто вызывает: на неотсортированном
списке bisect вернёт неверный индекс без ошибки — последняя строка прогона
выше. Поэтому O(log n) поиска
стоит рядом с O(n log n) сортировки: двоичный поиск окупается, когда список
сортируют один раз, а ищут в нём много раз.
И ещё одно: bisect_left возвращает позицию вставки, а не ответ «есть
ли элемент». 17 сравнений в таблице выше — только __lt__ до этой позиции.
Найден ли элемент, решает отдельная проверка: позиция внутри списка и
элемент на ней равен искомому — i < len(a) and a[i] == x. Вывод
bench/complexity/basics.py, раздел 7:
7) bisect: позиция вставки ещё не «нашли» — нужна проверка после неё
ищем 199998 в 0, 2, 4, …, 199998: позиция 99999, __lt__ — 17, __eq__ — 1, найден: True
ищем 77777 в 0, 2, 4, …, 199998: позиция 38889, __lt__ — 17, __eq__ — 1, найден: False
ищем 200001 в 0, 2, 4, …, 199998: позиция 100000, __lt__ — 16, __eq__ — 0, найден: False
Для 77 777, которого в списке нет, bisect_left всё равно вернул позицию —
там стоит 77 778; без проверки равенства это выглядело бы как находка. Для
200 001 позиция равна длине списка, и сравнивать не с чем — отсюда проверка
границы.
То же в коротком примере, который можно скопировать. Полный файл
bench/complexity/find.py:
import bisect
import sys
print("PY", sys.version.split()[0])
def contains(a, x):
i = bisect.bisect_left(a, x)
return i < len(a) and a[i] == x
a = [2, 4, 6, 8]
for x in (6, 5, 9):
print(f"x = {x}: bisect_left -> {bisect.bisect_left(a, x)}, contains -> {contains(a, x)}")Запуск — python3.13 bench/complexity/find.py; вывод на CPython 3.13.7 (на
3.11, 3.12 и 3.14 — тот же, кроме строки версии):
PY 3.13.7
x = 6: bisect_left -> 2, contains -> True
x = 5: bisect_left -> 2, contains -> False
x = 9: bisect_left -> 4, contains -> False
Для 6 и для 5 позиция одна и та же — 2; отличает их только проверка равенства.
Механизм 2: append — амортизированное O(1)
Список хранит ссылки на элементы в одном куске памяти. Когда место
кончается, нужен кусок больше, и все ссылки, возможно, придётся скопировать —
это O(n). Если бы так было на
каждом append, n добавлений стоили бы O(n²). Поэтому CPython выделяет
память с запасом; прогон ловит каждое перевыделение по изменению
sys.getsizeof — вывод bench/complexity/basics.py, раздел 2:
2) append: сколько раз список перевыделял память
первые ёмкости (мест под элементы): [4, 8, 16, 24, 32, 40, 52, 64, 76, 92, 108, 128]
1000 append — перевыделений: 28
10000 append — перевыделений: 47
100000 append — перевыделений: 66
Ряд ёмкостей совпадает с комментарием в исходниках CPython — 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ... — и там же сказано, зачем он такой: запаса
is enough to give linear-time amortized behavior over a long sequence of appends()
(хватает, чтобы длинная последовательность append() в сумме занимала линейное время).
Запас растёт вместе с размером, поэтому на сто тысяч append пришлось 66
перевыделений, а не сто тысяч.
Это и называется амортизированным O(1): отдельный append изредка
дорогой — он может скопировать весь список, — но в сумме n добавлений стоят O(n), то
есть O(1) в среднем на одно. Ответ «append — это O(1)» верен, но с оговоркой,
которую ждут: в худшем случае отдельный вызов — O(n).
Механизм 3: рост времени — то же, но по часам
O-нотация предсказывает, во сколько раз вырастет время, если n вырастет в
десять раз: O(1) — примерно никак, O(n) — примерно в десять. Замер на трёх
размерах — вывод bench/complexity/cost.py (CPython 3.13.7):
нс на операцию, медианы 7 кругов
операция 1000 10000 100000 ×10 ×100
list.append 35.1 26.3 28.4 0.8 0.8
list.insert(0, x) 308.2 2242.9 19988.0 7.3 64.9
x in list 5332.6 52136.7 643060.2 9.8 120.6
x in set 25.4 27.2 26.2 1.1 1.0
sorted(), на элемент 106.7 127.0 220.3 1.2 2.1
appendиx in set— время на операцию не растёт: это O(1) (амортизированное и среднее; уx in setздесь целые без коллизий).insert(0, x)иx in list— растут почти в десять раз на каждые ×10: O(n).insert(0, x)сдвигает все элементы,x in listперебирает их.sorted()на элемент растёт медленно — в 1,2 раза на ×10: это множитель log n из O(n log n).
Числа не ложатся на предсказание точно. У insert(0, x) — 7,3 вместо 10, но
это свойство замера: за тысячу вставок список дорастает до n + 1 000, и
средняя длина — 1 500 и 10 500, отношение которых около 7. У sorted() на
×100 — 2,1 вместо ожидаемых по логарифму 1,7. O-нотация отбрасывает
постоянные расходы и ничего не говорит о том, что происходит в железе; она
описывает форму роста, а не точное время. Поэтому на собеседовании говорят
«растёт линейно», а не «в десять раз медленнее».
Механизм 4: скрытый квадрат
Самый частый вопрос «оцените сложность» — про код, где цикл не виден.
[x for x in a if x in b] выглядит как один проход по a. Но если b —
список, каждый x in b — тоже проход. Вывод bench/complexity/basics.py,
раздел 3:
3) скрытый квадрат: [x for x in a if x in b], общих элементов нет
n b — список b — множество
100 10000 0
1000 1000000 0
n выросло в десять раз — сравнений стало в сто раз больше: это O(n²). С
множеством b вызовов __eq__ нет вовсе: хеши элементов a не совпадают
ни с одним хешем в множестве, а __eq__ вызывается только при совпадении
хеша. Построение множества — O(n) в среднем, поиск — O(1) в среднем на
элемент, итого O(n); при совпадающих хешах обе оценки хуже (раздел 6 выше).
Где ещё прячется квадрат: lst.remove(x) или lst.index(x) в цикле по
другой коллекции — for x in to_delete: lst.remove(x), — lst.insert(0, x)
в цикле, x in lst для отбора уникальных. Правило оценки одно — умножить число итераций цикла на
стоимость операции внутри него.
Глубже: сортировка и n − 1 сравнений
Вывод bench/complexity/basics.py, раздел 4:
4) sorted(): сколько сравнений
n случайные lg(n!) n·log2(n) отсортированные обратные
1000 8655 8529 9966 999 999
10000 120184 118458 132877 9999 9999
100000 1531824 1516704 1660964 99999 99999
На случайных данных сравнений около n·log2(n) — это O(n log n). Колонка
lg(n!) — теоретический минимум: никакая сортировка сравнениями не может в
среднем обойтись меньшим числом. Сортировка CPython на 3.13 отстаёт от него
на 1–1,5 %.
А на уже отсортированных и строго убывающих данных в прогоне вышло ровно
n − 1 сравнений, O(n). Это наблюдение на таком упорядоченном входе и этой
реализации, а не обещание языка; объясняет его устройство сортировки:
сортировка CPython ищет во входе готовые упорядоченные участки, убывающие
разворачивает и сливает их; если весь вход — один такой участок, достаточно
один раз пройти по нему. Поэтому ответ «sorted() работает за O(n log n)»
верен для худшего и среднего случая, а лучший — O(n).
Глубже: цена одного сравнения
Весь урок считает сравнения, как будто каждое стоит одинаково. Для целых это
близко к правде, для строк — нет: хеш и проверка равенства проходят по
символам. Вывод bench/complexity/strkeys.py (CPython 3.13.7):
строковые ключи: нс на операцию, медианы 7 кругов
операция 10 1000 100000
hash(новой строки) 35.3 431.8 29768.6
hash(той же строки) 34.6 34.6 30.1
a == b, разные объекты 21.3 36.0 2567.6
Хеш новой строки в сто тысяч символов — около 30 мкс, в десять символов —
35 нс; сравнение двух равных, но разных строк тоже растёт с длиной.
Повторный хеш той же строки дёшев при любой длине: строка запоминает его в
себе (поле hash в Include/cpython/unicodeobject.h:101, CPython 3.13.7).
Поэтому «поиск в множестве — O(1)» — это O(1) по числу элементов, а цена
одной проверки зависит от ключа: новую длинную строку надо захешировать, а
равную ей, но другую — сравнить посимвольно.
Как отвечать на собеседовании
Короткий ответ: O-нотация описывает, как растёт работа с ростом входа;
строго это верхняя оценка. x in list, insert(0, x), remove — O(n);
x in set, d[key] — O(1) в среднем и O(n) в худшем случае, когда хеши
совпадают; append — амортизированное O(1); bisect — O(log n);
sorted() — O(n log n) в среднем и худшем случае. Сложность кода — это
число итераций, умноженное на стоимость операции внутри.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
На «оцените [x for x in a if x in b]». «Если b список — O(n·m): каждый
in перебирает b. У меня на тысяче элементов вышел миллион сравнений.
Сделать b множеством — и будет O(n + m)».
На «почему append — O(1), если список иногда копируется». «Он растёт с запасом, пропорциональным размеру. На сто тысяч append было 66 перевыделений. Дорогие вызовы редки, в сумме — O(n), то есть O(1) в среднем. Это и есть амортизированная сложность».
На «какая сложность у sorted()». «O(n log n) в среднем и худшем случае. На уже отсортированных данных — O(n): у меня было ровно n − 1 сравнений, сортировка CPython находит готовые участки».
На «почему двоичный поиск быстрый». «Каждое сравнение отбрасывает
половину: на ста тысячах элементов — 17 сравнений вместо ста тысяч. Но
список должен быть отсортирован, а сортировка — O(n log n), так что это
выгодно при многих поисках. И bisect_left даёт позицию вставки — найден ли
элемент, проверяю отдельно: i < len(a) and a[i] == x».
На «x in set — это одно сравнение?» «В среднем O(1), но не одно
сравнение по определению. В моём прогоне у ключей без коллизий так и было,
а когда хеш у всех ста элементов один, поиск вызвал __eq__ 105 раз —
худший случай O(n). Это счётчик CPython, не гарантия языка».
Дальше спросят
Какая сложность у операций словаря?
Как у множества: поиск, вставка и удаление по ключу — O(1) в среднем (вставка — ещё и амортизированно: таблица иногда перевыделяется), потому что словарь тоже находит ключ по хешу. В худшем случае, когда хеши ключей совпадают, поиск вырождается в перебор — раздел 5 того же прогона, одинаковый на 3.11–3.14:
5) словарь, у всех ключей один и тот же хеш: сравнений на поиск последнего ключа
n = 10: обычные хеши — 1, один хеш на всех — 10
n = 100: обычные хеши — 1, один хеш на всех — 100
n = 1000: обычные хеши — 1, один хеш на всех — 1000
Поэтому плохой __hash__ — например, константа — превращает словарь в
список. Как словарь устроен внутри, разобрано в статье о словарях.
Что значит «O(1) в среднем» и «амортизированное O(1)» — это одно и то же?
Нет. «В среднем» — про разные входы: у словаря отдельный поиск может быть
медленным, если не повезло с хешами. «Амортизированное» — про
последовательность операций: дорогой append бывает, но редко, и сумма n
вызовов гарантированно O(n).
Зачем O-нотация, если всё решают константы?
Константы решают на маленьких n: на десяти элементах перебор списка медленнее множества всего в несколько раз — в уроке о коллекциях в 3,4. O-нотация отвечает на другой вопрос — что будет, когда данных станет в сто раз больше. Квадратичный код, незаметный на тестовых данных, на настоящих становится в сто раз медленнее при росте данных в десять раз.
Частые заблуждения
Однострочный генератор списка — это O(n)
Если внутри x in по списку — O(n²). На тысяче элементов [x for x in a if x in b] со списком b сделал миллион сравнений.
append всегда O(1)
Амортизированно. Отдельный append, на котором кончилось место, копирует весь список — O(n). Но таких вызовов мало: 66 на сто тысяч.
Сортировка всегда стоит O(n log n)
O(n log n) — для среднего и худшего случая. На уже отсортированных и строго убывающих данных sorted() в прогоне сделал ровно n − 1 сравнений — O(n); это наблюдение на таком входе, а не обещание.
x in set — ровно одно сравнение при любом n
Одно сравнение — наблюдение для ключей без коллизий хешей. O(1) — средняя оценка: когда хеш у всех ста элементов один, поиск вызвал __eq__ 105 раз, на десяти тысячах — 11 672. Худший случай — O(n).
O(1) значит «быстро», O(n) значит «медленно»
O-нотация говорит о росте, а не о скорости. x in list на десяти элементах — десять сравнений, и в уроке о коллекциях такой поиск был медленнее множества всего в 3,4 раза, а первый элемент список находил так же быстро.
История версий
| Версия | Изменение | Что это значит для кода |
|---|---|---|
| 3.11 | Опорная точка урока. Счёт сравнений при поиске, ряд ёмкостей списка и n − 1 сравнений на отсортированных данных одинаковы на 3.11–3.14. | |
| 3.13 | Число сравнений sorted() на тех же перемешанных данных стало чуть другим: 8 655 против 8 632 на 3.11 и 3.12 для тысячи элементов. Сложность та же — O(n log n); на точное число сравнений полагаться нельзя. | |
| 3.15 | Python 3.15 вышел 9 октября 2026. Урок его не запускал: все счётчики и замеры — CPython 3.11–3.14, граница сверена 14.10.2026. Изменений, касающихся темы урока, в этой строке не утверждается — на 3.15 счёт надо снять заново. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
n = 100
count = 0
for i in range(n):
for j in range(i + 1, n):
count += 1
print(count)Практика · оцените
Проверьте себя
Какая сложность у кода result = [x for x in a if x not in seen], где a — длины n, а seen — список длины m?
Чем измерено
Числа этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Счёт сравнений — CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7; замеры времени —
3.13.7 и 3.14.7 на одной машине с двумя ядрами. Разделы 6–7
bench/complexity/basics.py, bench/complexity/strkeys.py и
bench/complexity/find.py добавлены и запущены 14.10.2026 на linux/x86_64. Контракты и цитаты сверены 14.10.2026.
Python 3.15 (вышел 9 октября 2026) урок не запускал.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- O-нотация описывает не скорость, а то, как растёт работа с ростом входа. O(1) — работа не зависит от n, O(n) — растёт вместе с n, O(log n) — прибавляет понемногу при каждом удвоении, O(n²) — растёт как квадрат. Строго O — верхняя оценка («не быстрее, чем»), тесную оценку пишут Θ. Работу удобнее всего считать сравнениями: прогон считает их напрямую.
- Отсюда главное: оценивают не строку кода, а то, что она делает внутри.
x in list— перебор: на ста тысячах элементов сто тысяч сравнений.x in set— O(1) в среднем: в нашем наборе без коллизий хешей это одно сравнение на всех проверенных n, но если у всех элементов один хеш, поиск среди ста элементов вызвал__eq__105 раз — худший случай O(n). Поэтому безобидный генератор списка[x for x in a if x in b]со спискомb— это O(n²): на тысяче элементов он сделал миллион сравнений, а с множествомb— ни одного. - Дальше — то, что отличает понимание от пересказа.
append— «амортизированное» O(1): на сто тысяч добавлений список перевыделил память 66 раз, потому что растёт с запасом. Двоичный поискbisectна ста тысячах — 17 сравнений — но это позиция вставки, «найден ли» решает ещё одна проверка. Аsorted()— O(n log n) в среднем и худшем случае, но на уже отсортированных данных в нашем прогоне сделал ровно n − 1 сравнений: сортировка CPython ищет готовые упорядоченные участки.
На самом деле
- Если внутри
x inпо списку — O(n²). На тысяче элементов[x for x in a if x in b]со спискомbсделал миллион сравнений. - Амортизированно. Отдельный
append, на котором кончилось место, копирует весь список — O(n). Но таких вызовов мало: 66 на сто тысяч. - O(n log n) — для среднего и худшего случая. На уже отсортированных и строго убывающих данных
sorted()в прогоне сделал ровно n − 1 сравнений — O(n); это наблюдение на таком входе, а не обещание. - Одно сравнение — наблюдение для ключей без коллизий хешей. O(1) — средняя оценка: когда хеш у всех ста элементов один, поиск вызвал
__eq__105 раз, на десяти тысячах — 11 672. Худший случай — O(n). - O-нотация говорит о росте, а не о скорости.
x in listна десяти элементах — десять сравнений, и в уроке о коллекциях такой поиск был медленнее множества всего в 3,4 раза, а первый элемент список находил так же быстро.
По версиям
- 3.11
- Опорная точка урока. Счёт сравнений при поиске, ряд ёмкостей списка и n − 1 сравнений на отсортированных данных одинаковы на 3.11–3.14.<
- 3.13
- Число сравнений
sorted()на тех же перемешанных данных стало чуть другим: 8 655 против 8 632 на 3.11 и 3.12 для тысячи элементов. Сложность та же — O(n log n); на точное число сравнений полагаться нельзя.< - 3.15
- Python 3.15 вышел 9 октября 2026. Урок его не запускал: все счётчики и замеры — CPython 3.11–3.14, граница сверена 14.10.2026. Изменений, касающихся темы урока, в этой строке не утверждается — на 3.15 счёт надо снять заново.<
Что разобрано
- База: что говорит O-нотация
- Механизм 1: поиск — O(n), O(1) и O(log n) в числах
- Механизм 2: append — амортизированное O(1)
- Механизм 3: рост времени — то же, но по часам
- Механизм 4: скрытый квадрат
- Глубже: сортировка и n − 1 сравнений
- Глубже: цена одного сравнения
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
4 ИСТОЧНИКА
- Objects/listobject.c — list_resizeИсходный код CPython. Строки 130–136 на теге v3.13.7 (на v3.14.0 — 123–129, текст тот же). Комментарий к выделению памяти с запасом: «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()» (Память выделяется с запасом, пропорциональным размеру списка, — с местом для дальнейшего роста. Запас небольшой, но его хватает, чтобы длинная последовательность append() в сумме занимала линейное время даже при плохо работающем системном realloc()). И ряд ёмкостей: «The growth pattern is: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ...» (Ёмкость растёт так: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, …).https://github.com/python/cpython/blob/v3.13.7/Objects/listobject.c
- Objects/listsort.txt — описание сортировки CPythonИсходный код CPython. На теге v3.13.7 (на v3.14.0 файл тот же). Строки 59–60 — обозначения убывающих и возрастающих данных; строки 68–69: на них сортировка тривиальна, «because it naturally works on runs» (потому что она по своей природе работает с упорядоченными участками); строки 74–77 — таблица, где для n = 32 768 это 32 767 сравнений; строки 100–101 — оговорка, что на убывающих данных сравнений на одно меньше, чем у прежнего алгоритма. Строки 104–106: «lg(n!) is the information-theoretic limit for the best any comparison-based sorting algorithm can do on average (across all permutations)» (lg(n!) — теоретико-информационный предел того, что в среднем по всем перестановкам может сделать лучший алгоритм сортировки сравнениями). Строки 215–216 — как ищутся участки: «count_run() returns the # of elements in the next run, and, if it's a descending run, reverses it in-place» (count_run() возвращает число элементов в следующем участке и, если участок убывающий, разворачивает его на месте).https://github.com/python/cpython/blob/v3.13.7/Objects/listsort.txt
- bisect — строка документации bisect_leftОфициальная документация. Строка документации bisect.bisect_left в интерпретаторе: «Return the index where to insert item x in list a, assuming a is sorted» (Вернуть индекс, на который надо вставить x в список a, считая, что a отсортирован). Сверено вызовом bisect.bisect_left.__doc__ на 3.13.7 и 3.14.7.https://docs.python.org/3.13/library/bisect.html
- What’s New In Python 3.15Официальная документация. Python 3.15 вышел 9 октября 2026. Урок его не запускал: все счётчики и замеры — CPython 3.11–3.14; граница сверена 14.10.2026.https://docs.python.org/3.15/whatsnew/3.15.html