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

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

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 добавило бы всего один шаг.

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

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

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

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

В стандартной библиотеке PHP готового бинарного поиска нет. Встроенная функция array_search() находит значение, просматривая массив с начала, — это линейный поиск, O(n). На отсортированных данных написанный вручную бинарный поиск быстрее, и он ещё может сказать, куда попало бы отсутствующее значение.

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

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

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

Бинарный поиск против array_search()

binarySearch() находит 9 по индексу 4. Для отсутствующего значения 10 получаем найдено: false и индекс 5 — место, куда встало бы 10, чтобы массив остался отсортированным. array_search() тоже даёт ответ, но просматривает массив линейно, а для отсутствующего 10 возвращает только false — без позиции.

Prompt
Напишите PHP-функцию binarySearch, которая возвращает индекс и флаг найденности для значения в отсортированном массиве 1, 3, 5, 7, 9, 11, 13, 15, причём для отсутствующего значения индексом является позиция, куда его следовало бы вставить. Найдите 9 и отсутствующее 10, выведите оба результата, затем выполните array_search для тех же двух значений для сравнения.
Вывод: PHP-скрипт
Binary search vs array_search()

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

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

В PHP сортировку редко пишут сами — функция sort() имеет сложность O(n log n). Она принимает массив по ссылке и сортирует его на месте: меняется сам массив, ничего не нужно присваивать обратно. Один раз отсортировать, а затем много раз применять бинарный поиск — распространённый и быстрый подход.

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

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

Prompt
Напишите PHP-скрипт, который сортирует массив 31, 5, 17, 1, 23, 9, 27, 13 с помощью sort и выводит отсортированный массив.
Вывод: PHP-скрипт
Sort an array