1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 1 из 7

Степени, логарифмы и порядок роста

Коротко

Степень 2^k - это k удвоений: 2^10 около тысячи, 2^20 около миллиона, 2^30 около миллиарда. Логарифм идёт в обратную сторону: log₂ n - сколько раз n надо поделить пополам, чтобы дойти до единицы. Поэтому алгоритм, который на каждом шаге отбрасывает половину данных, делает около log₂ n шагов: на миллионе - двадцать, на миллиарде - тридцать. Порядок роста - то, как число шагов растёт вместе с размером данных; он важнее любой константы. log n, n, n log n, n², 2ⁿ - пять ступеней, между соседними на больших данных пропасть.

Если ты без калькулятора знаешь, что log₂ миллиона около двадцати, а 2^n при n = 100 не посчитать никогда, листай до «Теперь сам» в конце.

Как думаешь, сколько нужно вопросов

Я загадал число от одного до миллиарда. Ты задаёшь вопросы «больше ли оно, чем ...?», я отвечаю «да» или «нет». Ответь себе до того, как читать дальше: сколько вопросов понадобится в худшем случае? Тысяча? Миллион? Ответ - тридцать, и вся первая половина урока объясняет, откуда это число и почему оно такое маленькое.

На что это похоже

Турнир на выбывание. В каждом круге играют пары, и половина участников уходит. При 1024 участниках кругов будет 10: 1024, 512, 256, ..., 2, 1. Добавь участников вдвое - кругов станет на ОДИН больше. Число кругов турнира - это и есть логарифм числа участников. А обратная задача - сколько участников выдержит турнир из k кругов - это степень: каждый лишний круг удваивает число мест.

Механизм: степень и логарифм - один счёт в две стороны

Степень 2^k - произведение k двоек: 2^3 = 2 · 2 · 2 = 8. В Python пишется 2 ** k, в тексте курса иногда 2³ или 2ⁿ. Удвоение растёт пугающе быстро, и три опорные точки стоит помнить наизусть:

k 2^k примерно
10 1 024 тысяча
20 1 048 576 миллион
30 1 073 741 824 миллиард
64 18 446 744 073 709 551 616 18 · 10^18

Логарифм по основанию два - обратный счёт: log₂ n это такое k, что 2^k = n, то есть сколько раз n делится пополам до единицы. log₂ 8 = 3, потому что 8 → 4 → 2 → 1. Для n, которое не является степенью двойки, логарифм дробный (log₂ 1000 ≈ 9.97), а число делений нацело - это логарифм, округлённый вниз. Округление вниз пишут ⌊x⌋, вверх - ⌈x⌉: ⌊9.97⌋ = 9, ⌈9.97⌉ = 10.

Почему в оценках алгоритмов стоит именно логарифм, а не что-то другое: деление пополам до единицы и ЕСТЬ логарифм по определению. Где алгоритм на каждом шаге отбрасывает половину - двоичный поиск, сбалансированное дерево, турнир, - там и появляется log₂ n.

Два свойства понадобятся постоянно. log₂(a · b) = log₂ a + log₂ b, потому что при умножении степеней показатели складываются: 2^x · 2^y = 2^(x+y). Отсюда удвоить данные значит добавить один шаг: log₂(2n) = 1 + log₂ n. И основание почти не важно: n = 10^(log₁₀ n), а 10 ≈ 2^3.32, поэтому log₂ n ≈ 3.32 · log₁₀ n - логарифмы по разным основаниям отличаются в постоянное число раз, и в оценках часто пишут просто log n.

Правило

2^k - k удвоений, log₂ n - сколько раз делить n пополам до единицы; одно обратно другому. 2^10 ≈ 10^3, отсюда log₂ тысячи около 10, миллиона около 20, миллиарда около 30. Порядок роста - как число шагов растёт с размером данных, и на больших данных он решает всё, а константа - почти ничего.

Разбор: сколько раз делится пополам

Проверим счёт делений на настоящих числах. Встроенный n.bit_length() - 1 даёт тот же ответ другим способом - через число двоичных цифр, о которых следующий урок:

popolam.py
for n in [1, 2, 8, 1000, 10 ** 6, 10 ** 9]:
    k = 0
    m = n
    while m > 1:
        m = m // 2
        k = k + 1
    print(n, k, n.bit_length() - 1)
↳  выводpython3 popolam.py
1 0 0
2 1 1
8 3 3
1000 9 9
1000000 19 19
1000000000 29 29

Миллиард делится пополам нацело 29 раз, и тридцатый вопрос в игре «угадай число» нужен, чтобы из двух оставшихся кандидатов выбрать один. Обрати внимание: данные выросли в миллиард раз, а число шагов - с нуля до 29. Это и делает логарифмические алгоритмы такими ценными.

Разбор: пять ступеней роста

Сравним, сколько шагов нужно алгоритмам разного порядка роста. n log n получается, когда на каждый из n элементов уходит около log n шагов: так работают лучшие сортировки, о них свой раздел. У 2ⁿ число такое, что печатаем только, сколько в нём цифр. Модуль math входит в сам Python, ставить его не надо:

rost.py
import math

print("       n  log n    n log n             n²  2ⁿ: цифр")
for n in [10, 100, 1000, 10 ** 6]:
    lg = math.log2(n)
    cifr = int(n * math.log10(2)) + 1
    print(f"{n:>8} {lg:>6.1f} {n * lg:>10.0f} {n * n:>14} {cifr:>9}")
print(math.log(1000, 10), int(math.log(1000, 10)))
↳  выводpython3 rost.py
       n  log n    n log n             n²  2ⁿ: цифр
      10    3.3         33            100         4
     100    6.6        664          10000        31
    1000   10.0       9966        1000000       302
 1000000   19.9   19931569  1000000000000    301030
2.9999999999999996 2

Запись f"{n:>8}" - строка, в которую вставлено значение n, выровненное вправо по ширине 8; .1f добавляет «одна цифра после точки», .0f - «без дробной части». Число цифр у 2ⁿ программа считает без самого 2ⁿ: у числа m десятичных цифр ⌊log₁₀ m⌋ + 1, а log₁₀ 2ⁿ = n · log₁₀ 2 по тому же правилу «логарифм произведения - сумма логарифмов». Последняя строка вывода к таблице не относится - о ней ниже. Переведём таблицу в секунды на машине, делающей миллиард простых шагов в секунду. На миллионе элементов n log n - две сотых секунды, n² - около семнадцати минут. А 2ⁿ уже при n = 100 - число из 31 цифры: столько шагов не сделать за время существования Вселенной. Разница между соседними ступенями на больших данных - не «в несколько раз», а «готово сразу» против «не дождаться».

Что ломается без этого

Последняя строка вывода rost.py - 2.9999999999999996 2. Логарифм тысячи по основанию десять равен трём, но дробные числа хранятся неточно, и int() отрезает дробную часть - получается 2. Для целых логарифмов по основанию два надёжнее n.bit_length() - 1 или цикл делением, как в первом разборе. Вторая ошибка - путать округления: двоичных цифр у числа ⌊log₂ n⌋ + 1, а не ⌈log₂ n⌉, и на степенях двойки эти формулы расходятся (у 8 цифр четыре: 1000). Третья - считать экспоненту «просто большим числом» и надеяться на быструю машину: удвоение скорости машины даёт алгоритму 2ⁿ ровно один лишний элемент.

Зачем это в работе

Оценка на салфетке до написания кода. Индекс по миллиарду записей - около тридцати сравнений на поиск, значит, вопрос не в процессоре, а в том, сколько из этих тридцати сходят на диск. Сравнение каждого пользователя с каждым на миллионе пользователей - полтриллиона разных пар, и это не «оптимизировать», а передумать. Перебор всех подмножеств из сорока признаков - триллион вариантов, из шестидесяти - невозможно. Привычка прикидывать log, n² и 2ⁿ в уме отсекает плохое решение раньше, чем на него потрачен день.

Теперь сам

Сколько раз можно поделить 5000 пополам нацело, пока не останется единица? И во сколько раз вырастет время алгоритма с порядком n², если данных станет в десять раз больше?

Ответ. 2^12 = 4096 ≤ 5000 < 8192 = 2^13, значит, ⌊log₂ 5000⌋ = 12: двенадцать делений (5000, 2500, 1250, 625, 312, 156, 78, 39, 19, 9, 4, 2, 1). Для n²: (10n)² = 100 n² - примерно в сто раз (ровно - если время равно n², а младшие слагаемые на больших данных почти ничего не меняют). У линейного алгоритма было бы в десять, у логарифмического - плюс три-четыре шага.

Главное

Логарифм - это число делений пополам, а степень - число удвоений, и поэтому log₂ миллиарда всего тридцать, а 2ⁿ при сотне элементов уже недосягаемо. Порядок роста, а не скорость машины, решает, закончится ли алгоритм на больших данных.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.