Ответ 1
Я должен согласиться, что это довольно странно в первый раз, когда вы видите алгоритм O (log n)... откуда на самом деле возникает этот логарифм? Тем не менее, оказывается, что существует несколько различных способов, с помощью которых вы можете получить логарифмический термин, который будет отображаться в примечаниях с большим О. Вот несколько:
Повторное деление на константу
Возьмем любое число n; скажем, 16. Сколько раз вы можете разделить n на два, прежде чем вы получите число, меньшее или равное одному? Для 16 имеем, что
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
Обратите внимание, что это заканчивается для выполнения четырех шагов. Интересно, что у нас также есть log 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 я раз. Тогда получим число n/2 i. Если мы хотим решить для значения i, где это значение не более 1, получим
n/2 i & le; 1
n & le; 2 я
log 2 n & le; я
Другими словами, если мы выберем целое число я такое, что я & ge; log 2 n, то после деления n на половину я раз мы будем иметь значение, самое большее 1. Наименьший i, для которого это гарантировано, составляет примерно log 2 n, поэтому, если у нас есть алгоритм, который делит на 2 до тех пор, пока число не станет достаточно малым, мы можем сказать, что он заканчивается шагами O (log n).
Важная деталь заключается в том, что не имеет значения, какая константа вы делите n на (если она больше одного); если вы разделите на константу k, для достижения 1. потребуются шаги log k n. Таким образом, любой алгоритм, который многократно делит размер ввода на некоторую часть, потребует завершения 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) раз, прежде чем мы запустим из элементов массива.
Алгоритмы, следуя общей методике divide-and-conquer (разрезаем проблему на куски, решая эти части, а затем сведение проблемы вместе) имеют тенденцию иметь логарифмические термины в них по той же самой причине - вы не можете разрезать какой-либо объект наполовину больше, чем O (log n) раз. Вы можете посмотреть merge sort как отличный пример.
Обработка значений по одной цифре за раз
Сколько цифр в базовом номере 10? Ну, если в номере есть k цифр, тогда у нас будет самая большая цифра, кратная 10 k Наибольшее k-значное число составляет 999... 9, k раз, и это равно 10 k + 1 - 1. Следовательно, если мы знаем, что n имеет k цифр в нем, то мы известно, что значение n не превышает 10 k + 1 - 1. Если мы хотим решить для k в терминах n, получим
n & le; 10 k + 1 - 1
n + 1 & le; 10 к + 1
log 10 (n + 1) & le; k + 1
(log 10 (n + 1)) - 1 & le; к
Из которого получаем, что k приблизительно является логарифмом базы 10. Другими словами, число цифр в 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
Наконец, мы добавляем четвертый-последний ( "preantepenultimate"... я люблю английский):
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) от работы по одной цифре за раз в некоторой базе. Классическим примером является radix sort, который сортирует целые числа по одной цифре за раз. Существует много разновидностей сортировки radix, но они обычно запускаются во времени O (n log U), где U - наибольшее возможное целое, которое сортируется. Причиной этого является то, что каждый проход сортировки принимает время O (n), и есть всего итераций O (log U), необходимых для обработки каждой из O (log U) цифр наибольшего числа, которое сортируется. Многие передовые алгоритмы, такие как алгоритм кратчайших путей Габова или масштабирующая версия Алгоритм Max-flow от Ford-Fulkerson, имеют логарифмический термин в своей сложности, потому что они работают по одной цифре за раз.
Что касается вашего второго вопроса о том, как вы решаете эту проблему, вы можете посмотреть этот связанный вопрос, в котором рассматривается более сложное приложение. Учитывая общую структуру проблем, описанных здесь, вы теперь можете лучше понять, как думать о проблемах, когда знаете, что в результате есть лог-термин, поэтому я бы посоветовал не смотреть на ответ, пока вы его не дали некоторые думали.
Надеюсь, это поможет!