ЗАМЕР
bench/complexity/basics.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/complexity
- Как запустить
python3.11 bench/complexity/basics.py python3.12 bench/complexity/basics.py python3.13 bench/complexity/basics.py python3.14 bench/complexity/basics.py python3.13 bench/complexity/cost.py python3.14 bench/complexity/cost.py python3.13 bench/complexity/strkeys.py python3.14 bench/complexity/strkeys.py python3.11 bench/complexity/find.py python3.12 bench/complexity/find.py python3.13 bench/complexity/find.py python3.14 bench/complexity/find.py python3.11 bench/complexity/practice.py python3.12 bench/complexity/practice.py python3.13 bench/complexity/practice.py python3.14 bench/complexity/practice.py
Запись прогона
Замеры для урока «Сложность операций»
| Скрипт | Что делает |
|---|---|
basics.py |
счёт без времени: сколько сравнений делают x in list, x in set и bisect; сколько раз список перевыделяет память на append; сколько сравнений у [x for x in a if x in b] со списком и множеством; сколько сравнений у sorted() на случайных, отсортированных и обратных данных; поиск в словаре, где у всех ключей один хеш |
cost.py |
время на операцию при n = 1 000, 10 000 и 100 000: append, insert(0, x), x in list, x in set, sorted() на элемент |
practice.py |
источник ответов двух практических задач урока |
strkeys.py |
цена hash и == у строк длиной 10, 1 000 и 100 000 символов |
find.py |
полный короткий пример урока: bisect_left даёт позицию вставки, «найден ли» — отдельная проверка |
python3.11 bench/complexity/basics.py
python3.12 bench/complexity/basics.py
python3.13 bench/complexity/basics.py
python3.14 bench/complexity/basics.py
python3.13 bench/complexity/cost.py
python3.14 bench/complexity/cost.py
python3.13 bench/complexity/strkeys.py
python3.14 bench/complexity/strkeys.py
python3.11 bench/complexity/find.py
python3.12 bench/complexity/find.py
python3.13 bench/complexity/find.py
python3.14 bench/complexity/find.py
python3.11 bench/complexity/practice.py
python3.12 bench/complexity/practice.py
python3.13 bench/complexity/practice.py
python3.14 bench/complexity/practice.py
Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом
версии (3.11.15, 3.12.3, 3.14.7). Сравнения считают сами элементы — объекты,
у которых __eq__ и __lt__ увеличивают счётчик; искомый элемент — новый
объект, равный последнему, иначе Python узнал бы его по тождеству. Счёт
сравнений — наблюдение CPython для этих объектов, а не контракт языка: при
коллизиях хешей он другой (раздел 6). Числа cost.py и strkeys.py — этой
машины (2 ядра; разделы 6–7 basics.py и strkeys.py сняты 14.10.2026 на
linux/x86_64); отношения зависят от данных, версии и машины, у себя их надо
мерить заново. CPython 3.15 (вышел 9 октября 2026) здесь не запускался.
Что получилось
Поиск (одинаково на 3.11–3.14): на 100 000 элементах x in list — 100 000
сравнений, x in set — 1 (у Key без коллизий), bisect — 17. bisect по неотсортированному
списку возвращает неверный индекс без ошибки.
append: ёмкости 4, 8, 16, 24, 32, 40, 52, 64, 76, 92, … — как в комментарии
в Objects/listobject.c; на 1 000 / 10 000 / 100 000 добавлений — 28 / 47 / 66
перевыделений.
Скрытый квадрат: [x for x in a if x in b] без общих элементов — 10 000
сравнений при n = 100 и 1 000 000 при n = 1 000, если b список; 0, если
множество.
sorted(): на случайных данных (зерно 42) — 8 655 / 120 184 / 1 531 824 сравнений при n = 1 000 / 10 000 / 100 000 на 3.13 и 3.14; на 3.11 и 3.12 — 8 632 / 119 836 / 1 529 011. lg(n!) — 8 529 / 118 458 / 1 516 704. На отсортированных и строго убывающих данных — n − 1 на всех версиях.
Словарь с одинаковыми хешами: поиск последнего ключа — n сравнений (10 / 100 / 1 000) против одного при обычных хешах.
Множество с одним хешем на всех (раздел 6, одинаково на 3.11–3.14):
поиск последнего — 10 / 105 / 1 336 / 11 672 вызовов __eq__ при n = 10 / 100 /
1 000 / 10 000 (хеш 1); с хешем 0 при n = 100 — 114. Построение — 45 / 5 982 /
605 201 / 57 720 235. Найденный по тождеству элемент — 0 вызовов __eq__.
bisect и «нашли» (раздел 7): bisect_left возвращает позицию вставки; для
77 777 в списке чётных — позиция 38 889 и __eq__ → False; для 200 001 —
позиция 100 000, равная длине, сравнивать не с чем. Полный короткий пример того же — find.py:
для [2, 4, 6, 8] у 6 и 5 позиция одна — 2, contains даёт True и False.
Строковые ключи (strkeys.py, медианы, 3.13.7, нс): hash новой строки
длиной 10 / 1 000 / 100 000 — 35,3 / 431,8 / 29 768,6; a == b у равных
разных объектов — 21,3 / 36,0 / 2 567,6. Повторный hash той же строки —
около 30–35 при любой длине: хеш запоминается в объекте.
Время (медианы, 3.13.7, нс на операцию при n = 1 000 / 10 000 / 100 000):
append 35,1 / 26,3 / 28,4; insert(0, x) 308,2 / 2 242,9 / 19 988,0;
x in list 5 332,6 / 52 136,7 / 643 060,2; x in set 25,4 / 27,2 / 26,2;
sorted() на элемент 106,7 / 127,0 / 220,3.
Скрипт
205 строк"""Сложность операций: O-нотация, посчитанная, а не заученная.
Времени здесь нет — только счёт. O-нотация говорит не о секундах, а о том,
как растёт объём работы с ростом n. Работу проще всего посчитать прямо:
сколько раз Python сравнил два элемента. Для этого элементы — объекты
класса Key, который считает вызовы своих __eq__ и __lt__. Время — в cost.py.
СЕМЬ СЮЖЕТОВ:
1. ПОИСК. `x in list` сравнивает элементы по очереди: n сравнений, если
элемент последний. `x in set` находит элемент по хешу: у Key без
коллизий — одно сравнение на всех проверенных n (это наблюдение для
этого набора, а не свойство множества — см. раздел 6). `bisect` по
отсортированному списку — около log2(n).
2. APPEND. Список выделяет память с запасом, поэтому перевыделений на
тысячи append — десятки, а не тысячи. Отсюда «амортизированное O(1)».
3. СКРЫТЫЙ КВАДРАТ. `[x for x in a if x in b]` — цикл внутри цикла, если
b — список: n × n сравнений. Тот же код с b-множеством — ноль сравнений
сверх хешей.
4. СОРТИРОВКА. sorted() на случайных данных делает около n·log2(n)
сравнений — близко к lg(n!), теоретическому минимуму сортировки
сравнениями в среднем, — а на уже отсортированных — ровно n − 1.
5. ХУДШИЙ СЛУЧАЙ СЛОВАРЯ. Если у всех ключей одинаковый хеш, поиск по
ключу перестаёт быть O(1) и сравнивает ключи по очереди.
6. ХУДШИЙ СЛУЧАЙ МНОЖЕСТВА. То же для set: при одном хеше на всех поиск
вызывает __eq__ порядка n раз (и больше n — множество проверяет часть
ячеек повторно), а построение — порядка n²/2. Найденный по тождеству
элемент __eq__ не вызывает вовсе. Счётчики — наблюдение CPython, не
контракт языка.
7. BISECT И «НАШЛИ». bisect_left возвращает позицию вставки; найден ли
элемент, решает отдельная проверка границы и равенства после неё.
Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import bisect
import math
import random
import sys
print("PY", sys.version.split()[0])
print()
COUNT = {"eq": 0, "lt": 0}
class Key:
"""Целое число, которое считает, сколько раз его сравнили."""
__slots__ = ("v",)
def __init__(self, v):
self.v = v
def __eq__(self, other):
COUNT["eq"] += 1
return self.v == other.v
def __lt__(self, other):
COUNT["lt"] += 1
return self.v < other.v
def __hash__(self):
return hash(self.v)
def counted(fn):
COUNT["eq"] = COUNT["lt"] = 0
fn()
return COUNT["eq"] + COUNT["lt"]
# ── 1 ─────────────────────────────────────────────────────────────────────
print("1) поиск: сколько сравнений, искомый элемент последний")
print(f" {'n':>7}{'x in list':>12}{'x in set':>10}{'bisect':>8}{'log2(n)':>9}")
for n in (10, 100, 1_000, 10_000, 100_000):
lst = [Key(i) for i in range(n)]
st = set(lst)
# Искомый — новый объект, равный последнему: иначе Python узнал бы его
# по тождеству и не стал бы вызывать __eq__.
target = Key(n - 1)
c_list = counted(lambda: target in lst)
c_set = counted(lambda: target in st)
c_bis = counted(lambda: bisect.bisect_left(lst, target))
print(f" {n:>7}{c_list:>12}{c_set:>10}{c_bis:>8}{math.log2(n):>9.1f}")
unsorted = [5, 1, 4, 2, 3]
print(f" bisect на неотсортированном {unsorted}, ищем 1: индекс {bisect.bisect_left(unsorted, 1)},"
f" а 1 стоит на месте {unsorted.index(1)} — ошибки нет")
print()
# ── 2 ─────────────────────────────────────────────────────────────────────
print("2) append: сколько раз список перевыделял память")
EMPTY = sys.getsizeof([])
lst = []
capacities = []
last = sys.getsizeof(lst)
reallocs = {}
for i in range(1, 100_001):
lst.append(i)
size = sys.getsizeof(lst)
if size != last:
capacities.append((size - EMPTY) // 8)
last = size
if i in (1_000, 10_000, 100_000):
reallocs[i] = len(capacities)
print(" первые ёмкости (мест под элементы):", capacities[:12])
for n, r in reallocs.items():
print(f" {n:>7} append — перевыделений: {r}")
print()
# ── 3 ─────────────────────────────────────────────────────────────────────
print("3) скрытый квадрат: [x for x in a if x in b], общих элементов нет")
print(f" {'n':>7}{'b — список':>14}{'b — множество':>16}")
for n in (100, 1_000):
a = [Key(i) for i in range(n)]
b_list = [Key(i) for i in range(n, 2 * n)]
b_set = set(b_list)
c_list = counted(lambda: [x for x in a if x in b_list])
c_set = counted(lambda: [x for x in a if x in b_set])
print(f" {n:>7}{c_list:>14}{c_set:>16}")
print()
# ── 4 ─────────────────────────────────────────────────────────────────────
print("4) sorted(): сколько сравнений")
print(f" {'n':>7}{'случайные':>11}{'lg(n!)':>9}{'n·log2(n)':>11}{'отсортированные':>17}{'обратные':>10}")
rng = random.Random(42)
for n in (1_000, 10_000, 100_000):
data = [Key(i) for i in range(n)]
shuffled = data[:]
rng.shuffle(shuffled)
c_rand = counted(lambda: sorted(shuffled))
c_sorted = counted(lambda: sorted(data))
c_rev = counted(lambda: sorted(data[::-1]))
lg_fact = round(math.lgamma(n + 1) / math.log(2))
print(f" {n:>7}{c_rand:>11}{lg_fact:>9}{round(n * math.log2(n)):>11}{c_sorted:>17}{c_rev:>10}")
print()
# ── 5 ─────────────────────────────────────────────────────────────────────
print("5) словарь, у всех ключей один и тот же хеш: сравнений на поиск последнего ключа")
class SameHash(Key):
__slots__ = ()
def __hash__(self):
return 1
for n in (10, 100, 1_000):
normal = {Key(i): i for i in range(n)}
bad = {SameHash(i): i for i in range(n)}
c_normal = counted(lambda: normal[Key(n - 1)])
c_bad = counted(lambda: bad[SameHash(n - 1)])
print(f" n = {n:>5}: обычные хеши — {c_normal}, один хеш на всех — {c_bad}")
print()
# ── 6 ─────────────────────────────────────────────────────────────────────
# Добавлено 14.10.2026 по внешнему аудиту (PY-CMP-01): «одно сравнение у
# x in set» в разделе 1 — наблюдение для Key без коллизий, а не свойство
# множества. Здесь то же множество, но у всех элементов один хеш.
print("6) множество, у всех элементов один и тот же хеш: вызовов __eq__")
print(f" {'n':>7}{'обычные хеши':>14}{'один хеш: поиск':>17}{'один хеш: построение':>22}")
for n in (10, 100, 1_000, 10_000):
normal = set(Key(i) for i in range(n))
COUNT["eq"] = 0
bad = set(SameHash(i) for i in range(n))
c_build = COUNT["eq"]
c_normal = counted(lambda: Key(n - 1) in normal)
c_bad = counted(lambda: SameHash(n - 1) in bad)
print(f" {n:>7}{c_normal:>14}{c_bad:>17}{c_build:>22}")
class ZeroHash(Key):
__slots__ = ()
def __hash__(self):
return 0
zero = set(ZeroHash(i) for i in range(100))
print(" тот же опыт с хешем 0 вместо 1, n = 100: поиск —", counted(lambda: ZeroHash(99) in zero))
same_obj = [Key(i) for i in range(100)]
same_set = set(same_obj)
print(" обычные хеши, искомый — тот же объект, а не равный ему: поиск —",
counted(lambda: same_obj[-1] in same_set))
print()
# ── 7 ─────────────────────────────────────────────────────────────────────
# bisect_left возвращает позицию вставки, а не ответ «нашли»: найден ли
# элемент, решает отдельная проверка границы и равенства после неё.
print("7) bisect: позиция вставки ещё не «нашли» — нужна проверка после неё")
evens = [Key(2 * i) for i in range(100_000)]
for x in (199_998, 77_777, 200_001):
COUNT["eq"] = COUNT["lt"] = 0
i = bisect.bisect_left(evens, Key(x))
c_lt = COUNT["lt"]
found = i < len(evens) and evens[i] == Key(x)
print(f" ищем {x:>7} в 0, 2, 4, …, 199998: позиция {i:>6}, __lt__ — {c_lt},"
f" __eq__ — {COUNT['eq']}, найден: {found}")