Я должен согласиться, что это довольно странно, когда вы в первый раз видите алгоритм O (log n) ... откуда вообще этот логарифм? Однако оказывается, что есть несколько разных способов получить логический термин, который будет отображаться в нотации с большим O. Вот несколько:
Повторяющееся деление на константу
Возьмите любое число n; скажем, 16. Сколько раз вы можете разделить n на два, прежде чем получите число, меньшее или равное единице? Для 16 у нас есть это
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
Обратите внимание, что для завершения требуется четыре шага. Интересно, что у нас также есть лог 2 16 = 4. Хммм ... а как насчет 128?
128 / 2 = 64
64 / 2 = 32
32 / 2 = 16
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
Это заняло семь шагов, и log 2 128 = 7. Случайно ли это? Нет! Для этого есть веская причина. Предположим, что мы делим число n на 2 i раз. Тогда получаем число n / 2 i . Если мы хотим найти значение i, где это значение не больше 1, мы получим
п / 2 я ≤ 1
п ≤ 2 я
журнал 2 n ≤ я
Другими словами, если мы выберем целое число i такое, что i ≥ log 2 n, то после деления n пополам i раз мы получим значение, не превышающее 1. Наименьшее i, для которого это гарантировано, примерно равно log 2. n, поэтому, если у нас есть алгоритм, который делит на 2, пока число не станет достаточно маленьким, то мы можем сказать, что он завершается за O (log n) шагов.
Важная деталь заключается в том, что не имеет значения, на какую константу вы делите n (если она больше единицы); если разделить на константу k, потребуется log k n шагов, чтобы достичь 1. Таким образом, любой алгоритм, который многократно делит входной размер на некоторую дробь, потребует O (log n) итераций для завершения. Эти итерации могут занять много времени, поэтому чистое время выполнения не обязательно должно быть O (log n), но количество шагов будет логарифмическим.
Так где это возникает? Одним из классических примеров является двоичный поиск , быстрый алгоритм поиска значения в отсортированном массиве. Алгоритм работает так:
- Если массив пуст, верните, что элемент отсутствует в массиве.
- В противном случае:
- Посмотрите на средний элемент массива.
- Если он равен искомому элементу, вернуть успех.
- Если он больше искомого элемента:
- Выбросьте вторую половину массива.
- Повторение
- Если он меньше искомого элемента:
- Выбросьте первую половину массива.
- Повторение
Например, чтобы найти 5 в массиве
1 3 5 7 9 11 13
Сначала посмотрим на средний элемент:
1 3 5 7 9 11 13
^
Поскольку 7> 5 и поскольку массив отсортирован, мы точно знаем, что число 5 не может находиться в задней половине массива, поэтому мы можем просто отбросить его. Это оставляет
1 3 5
Итак, теперь мы посмотрим на средний элемент здесь:
1 3 5
^
Поскольку 3 <5, мы знаем, что 5 не может появиться в первой половине массива, поэтому мы можем выбросить первую половину массива, чтобы оставить
5
Снова посмотрим на середину этого массива:
5
^
Поскольку это именно то число, которое мы ищем, мы можем сообщить, что 5 действительно находится в массиве.
Так насколько это эффективно? Что ж, на каждой итерации мы выбрасываем как минимум половину оставшихся элементов массива. Алгоритм останавливается, как только массив становится пустым или мы находим желаемое значение. В худшем случае элемента нет, поэтому мы продолжаем уменьшать размер массива вдвое, пока у нас не закончатся элементы. Как долго это займет? Что ж, поскольку мы продолжаем разрезать массив пополам снова и снова, мы сделаем не более O (log n) итераций, поскольку мы не можем разрезать массив пополам больше, чем O (log n) раз, прежде чем мы запустим вне элементов массива.
Алгоритмы следуя общей методике разделяй и властвуй (отрезания проблему на части, решая эти кусочки, затем положить эту проблему вместе) , как правило, имеют логарифмические члены в них по той же причине - вы не можете держать резки некоторый объект вдвое больше, чем O (log n) раз. Вы можете посмотреть на сортировку слиянием как на отличный пример.
Обработка значений по одной цифре за раз
Сколько цифр в десятичном числе n? Что ж, если в числе k цифр, то самая большая цифра кратна 10 k . Наибольшее k-значное число равно 999 ... 9, k раз, и это равно 10 k + 1 - 1. Следовательно, если мы знаем, что n состоит из k цифр, то мы знаем, что значение n равно не более 10 k + 1 - 1. Если мы хотим найти k через n, мы получим
п ≤ 10 к + 1 - 1
п + 1 ≤ 10 к + 1
войти 10 (п + 1) ≤ к + 1
(журнал 10 (n + 1)) - 1 ≤ k
Отсюда мы получаем, что k - это приблизительно десятичный логарифм числа n. Другими словами, количество цифр в n равно O (log n).
Например, давайте подумаем о сложности сложения двух больших чисел, которые слишком велики, чтобы уместиться в машинное слово. Предположим, что у нас есть эти числа, представленные в базе 10, и мы назовем числа m и n. Один из способов сложить их - использовать метод начальной школы: записывать числа по одной цифре за раз, а затем работать справа налево. Например, чтобы сложить 1337 и 2065, мы должны начать с записи чисел как
1 3 3 7
+ 2 0 6 5
==============
Складываем последнюю цифру и переносим 1:
1
1 3 3 7
+ 2 0 6 5
==============
2
Затем мы добавляем предпоследнюю («предпоследнюю») цифру и переносим 1:
1 1
1 3 3 7
+ 2 0 6 5
==============
0 2
Затем мы добавляем предпоследнюю («предпоследнюю») цифру:
1 1
1 3 3 7
+ 2 0 6 5
==============
4 0 2
Наконец, мы добавляем предпоследнюю ("предпоследнюю" ... я люблю английский) цифру:
1 1
1 3 3 7
+ 2 0 6 5
==============
3 4 0 2
Итак, сколько работы мы сделали? Мы выполняем в общей сложности O (1) работы на цифру (то есть постоянный объем работы), и есть O (max {log n, log m}) цифр, которые необходимо обработать. Это дает в общей сложности O (max {log n, log m}) сложность, потому что нам нужно посетить каждую цифру в двух числах.
Многие алгоритмы получают член O (log n) в результате обработки одной цифры за раз в некоторой базе. Классическим примером является сортировка по основанию счисления , при которой целые числа сортируются по одной цифре за раз. Существует много разновидностей сортировки по основанию, но обычно они выполняются за время O (n log U), где U - это наибольшее возможное целое число, которое сортируется. Причина этого в том, что каждый проход сортировки занимает O (n) времени, а для обработки каждой из O (log U) цифр наибольшего сортируемого числа требуется всего O (log U) итераций. Многие продвинутые алгоритмы, такие как алгоритм кратчайших путей Габоу или масштабируемая версия алгоритма максимального потока Форда-Фулкерсона , имеют логарифмический член в своей сложности, потому что они работают с одной цифрой за раз.
Что касается вашего второго вопроса о том, как вы решаете эту проблему, вы можете посмотреть на этот связанный вопрос, который исследует более сложное приложение. Учитывая общую структуру описанных здесь проблем, теперь вы можете лучше понимать, как думать о проблемах, когда знаете, что в результате есть логический термин, поэтому я бы посоветовал не смотреть на ответ, пока вы его не дадите. некоторые мысли.
Надеюсь это поможет!
O(log n)
можно рассматривать как: Если вы удвоите размер проблемыn
, вашему алгоритму потребуется лишь на постоянное количество шагов больше.