Перейти к основному содержимому

Делим задачу пополам

O(log n) означает логарифмическое время: каждый шаг уменьшает оставшуюся работу вдвое. Поскольку деление пополам очень быстро уменьшает числа, удвоение входных данных добавляет всего один лишний шаг.

Для 16 элементов нужно 4 деления пополам, для 32 — 5, для миллиона — около 20, для миллиарда — около 30. Вот почему логарифмические алгоритмы работают практически мгновенно даже на огромных данных.

Классический пример — бинарный поиск, но он работает только с отсортированными данными. Чтобы делить пополам, нужен порядок: только так можно понять, какую половину отбросить.


Цикл с делением пополам

Проще всего увидеть O(log n) на цикле, который делит n на 2, пока не дойдёт до 1. Количество повторений равно логарифму n: при n = 16 цикл выполняется 4 раза, потому что 2 · 2 · 2 · 2 = 16.

Делим пополам, пока не останется единица

16 превращается в 8, 4, 2, 1 — четыре шага. Если удвоить n до 32, добавится всего один шаг. Оператор // выполняет целочисленное деление, поэтому n остаётся целым числом (int).

Prompt
Напишите программу на Python, которая начинает с n = 16 и в цикле делит n на 2, пока не дойдёт до 1, выводя n после каждого деления и подсчитывая шаги. В конце выведите, сколько шагов потребовалось.
Вывод: полная программа на Python
Halving until one remains

Бинарный поиск

Бинарный поиск находит значение в отсортированном списке, каждый раз проверяя средний элемент. Если средний элемент слишком мал, вся левая половина отбрасывается; если слишком велик — правая. Каждая проверка вдвое сокращает диапазон поиска.

Два маркера, lo и hi, хранят текущий диапазон. Цикл завершается, когда значение найдено или диапазон стал пустым.

В Python есть и готовое решение: функция bisect.bisect_left() из стандартной библиотеки возвращает индекс, где значение должно находиться в отсортированном списке. Если значения нет, этот индекс указывает позицию, куда его можно вставить.

Бинарный поиск шаг за шагом

В отсортированном списке из 16 чисел поиск числа 13 требует всего 4 проверки: диапазон сокращается 16 → 8 → 4 → 2 и останавливается на индексе 6. Линейному поиску здесь понадобилось бы 7 шагов, а в худшем случае — до 16.

Prompt
Напишите программу на Python, которая выполняет бинарный поиск значения 13 в отсортированном списке 1, 3, 5, 7, ... 31 (нечётные числа, 16 элементов), используя маркеры lo и hi и подсчитывая проверки. Когда значение найдено, выведите его, его индекс и количество проверок.
Вывод: полная программа на Python
Binary search step by step

Бинарный поиск с модулем bisect

bisect.bisect_left() находит 9 по индексу 4, а сравнение nums[i] со значением подтверждает, что оно найдено. Для отсутствующего значения 10 получается индекс 5 — место, куда нужно вставить 10, чтобы список остался отсортированным, — и сравнение выводит False.

Prompt
Напишите программу на Python, которая использует bisect.bisect_left для отсортированного списка 1, 3, 5, 7, 9, 11, 13, 15. Найдите 9 и выведите индекс и флаг «найдено», затем найдите отсутствующее значение 10 и снова выведите оба результата.
Вывод: полная программа на Python
Binary search with the bisect module

O(n log n): сортировка

O(n log n) находится между линейным и квадратичным временем — это сложность хороших алгоритмов сортировки: данные снова и снова делятся пополам (log n уровней), и на каждом уровне выполняется линейный объём работы (n).

В Python редко приходится писать сортировку самому: nums.sort() упорядочивает список на месте, а sorted(nums) возвращает новый отсортированный список; оба варианта работают за O(n log n). Отсортировать один раз, а затем много раз выполнять бинарный поиск — распространённый и быстрый приём.

Сортировка списка

nums.sort() упорядочивает 8 чисел примерно за 8 · log 8 = 24 шага — гораздо меньше, чем 64 шага квадратичной сортировки, и разрыв быстро растёт по мере роста списка.

Prompt
Напишите программу на Python, которая сортирует список 31, 5, 17, 1, 23, 9, 27, 13 на месте с помощью метода sort() и выводит отсортированный список.
Вывод: полная программа на Python
Sort a list