Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2023 / 0 | Длительность: 63:16:00
Лекция 2:

Принципы анализа алгоритмов

О-нотация

Математическая запись, позволяющая отбрасывать детали при анализе алгоритмов, называется О-нотацией. Она определена следующим образом.

Определение 2.1. Говорят, что функция g(N) имеет порядок O(f(N)), если существуют такие постоянные с_0 и N_0, что 
            g (N) < c_0 f (N)
          для всех 
            N > N_0
          .

О-нотация используется по трем различным причинам:

  • Чтобы ограничить ошибку, возникающую при отбрасывании малых слагаемых в математических формулах.
  • Чтобы ограничить ошибку, возникающую при игнорировании частей программы, которые вносят небольшой вклад в анализируемую сумму.
  • Чтобы классифицировать алгоритмы по верхним границам их общего времени выполнения.

Третье назначение О-нотации рассматривается в разделе 2.7, а здесь мы обсудим два других.

Постоянные с_0 и N_0, не выраженные явно в О-нотации, часто скрывают практически важные подробности реализации. Очевидно, что выражение "алгоритм имеет время выполнения O(f (N)) " ничего не говорит о времени выполнения при N, меньшем N_0, а с_0 может иметь большое значение, необходимое для работы в наихудшем случае. Понятно, что лучше иметь алгоритм, время выполнения которого составляет N^2 наносекунд, а не log N столетий, но мы не можем сделать такой выбор на основе О-нотации.

Часто результаты математического анализа являются не точными, а приближенными именно в техническом смысле: результат представляет собой сумму убывающих слагаемых. Так же как мы интересуемся в основном внутренним циклом программы, нас интересует больше всего главные члены (наибольшие по величине слагаемые) математического выражения. О-нотация позволяет работать с приближенными математическими выражениями, рассматривая главные члены и опуская меньшие слагаемые, а также записывать краткие выражения, дающие неплохие приближения для анализируемых величин.

Некоторые из основных действий, которые используются при работе с выражениями, содержащими О-нотацию, являются предметом упражнений 2.20 - 2.25. Многие из этих действий интуитивно понятны, а склонные к математике читатели могут с интересом выполнить упражнение 2.21, где требуется доказать верность базовых операций, исходя из определения. По сути, из этих упражнений следует, что в алгебраических выражениях с О-нотацией можно раскрывать скобки так, как будто ее там нет, а затем отбрасывать все слагаемые, кроме наибольшего. Например, если требуется раскрыть скобки в выражении

(N + O (1))(N + O (log N) + O (1)), то мы получим шесть слагаемых


          N^2 + O (N) + O (N log N) + O (log N) + O (N) + O (1)
        .

Однако можно отбросить все О-слагаемые, кроме наибольшего из них, и тогда останется приближенное выражение


          N^2 + O (N log N)
        .

То есть при больших N хорошей аппроксимацией этого выражения является N^2. Эти действия интуитивно ясны, но О-нотация позволяет выразить их с математической точностью. Формула с одним О-слагаемым называется асимптотическим выражением (asymptotic expression).

В качестве более конкретного примера предположим, что (после некоторого математического анализа) мы выяснили, что определенный алгоритм имеет внутренний цикл, выполняемый в среднем 
          NH_N
        раз, внешний раздел, выполняемый N раз, и некоторый код инициализации, исполняемый однократно. Далее предположим, что (после тщательного исследования реализации) мы определили, что каждая итерация внутреннего цикла требует а_0 наносекунд, внешний раздел - а_1 наносекунд, а код инициализации - а_2 наносекунд. Тогда среднее время выполнения программы (в наносекундах) равно 
          2а_0 N H_N + a_1 + а_2
        .

Поэтому для времени выполнения справедлива следующая формула: 
          2а_0 NH_N + O(N)
        .

Эта более простая формула важна, поскольку из нее следует, что для аппроксимации времени выполнения при больших N нет необходимости искать значения величин 
          а_1 и а_2 . В общем случае, в точном математическом выражении для времени выполнения может содержаться множество других слагаемых, ряд которых трудно анализировать. О-нотация обеспечивает способ получения приближенного ответа для больших N, не заботясь о подобных слагаемых.

Далее, О-нотация позволяет в данном примере выразить время выполнения через более знакомую функцию lnN. С помощью таблица 2.3 полученное выражение можно приближенно записать как 
          H_N = lnN + O(1)
        . Таким образом, асимптотическое выражение для общего времени выполнения алгоритма имеет вид 
          2a_0lnN + O(N)
        . То есть при больших N оно будет близко к легко вычисляемому выражению 
          2a_0lnN
        . Постоянный множитель а0 зависит от времени выполнения инструкций внутреннего цикла.

Более того, нам не нужно знать значения a0, чтобы предсказать, что при больших N время выполнения для входных данных размером 2N будет вдвое больше, чем для входных данных размером N, поскольку

$\dfrac{2a_{0}(2N)\ln{#2N#}+O(2N)}{2a_{0}N\ln{N}+O(N)}=\dfrac{2\ln{(2N)}+O(1)}{\ln{N}+O(1)}=2+O\left(\dfrac{1}{\log{N}}\right)$.

Таким образом, асимптотическая формула позволяет нам делать точные прогнозы, не вдаваясь в подробности реализации или анализа. Отметьте, что такое предсказание не было бы возможным, если бы О-аппроксимация была задана только для главного члена.

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

Когда функция f (N) асимптотически велика по сравнению с другой функцией g (N) (т.е. $g(N)/f(N)\rightarrow0$ при $N\rightarrow\infty$), иногда в данной книге мы будем использовать термин (конечно, неточный) порядка f(N), что означает f (N) + O(g(N)). Потеря математической точности компенсируется большей наглядностью, так как нас больше интересует производительность алгоритмов, а не математические детали. В таких случаях мы можем быть уверены в том, что при больших (а, может, даже и всех) значениях N исследуемая величина будет близка к f(N). Например, даже если мы знаем, что некоторая величина равна N (N  -  1) / 2, ее можно рассматривать как 
          N^2/ 2
        . Такой способ выражения результатов более понятен, чем подробный и точный результат, и, к примеру, при N= 1000 отличается от правильного значения всего лишь на 0,1%. Потеря точности в данном случае намного меньше, чем при распространенном использовании O(f (N)). При описании производительности алгоритмов мы будем по возможности стараться быть и точными, и краткими.

В похожем ключе мы иногда говорим, что время выполнения алгоритма пропорционально f (N), т.е. можно доказать, что оно равно с f (N) + g(N), где g(N) асимптотически мало по сравнению с f (N). При таком подходе можно предсказать время выполнения для 2N, если оно известно для N, как в рассмотренном выше примере. На рис. 2.3 рис. 2.3 приводятся значения множителей для таких прогнозов поведения функций, которые часто возникают при анализе алгоритмов. В сочетании с эмпирическим изучением (см. раздел 2.1) данный подход освобождает от определения постоянных величин, зависящих от реализации. Или же, применяя его в обратном направлении, зачастую мы можем выдвинуть гипотезу о функциональной зависимости времени выполнения программы, изучив, как меняется время выполнения при удвоении N.

Различия между О-оценками пропорционально (is proportional to) и порядка (about) проиллюстрированы на рис. 2.4 и рис. 2.5. О-нотация используется, прежде всего, для исследования фундаментального асимптотического поведения алгоритма; пропорционально требуется при экстраполяции производительности на основе эмпирического изучения, а порядка - при сравнении производительности или при предсказании абсолютной производительности.

 Влияние удвоения размеров задачи на время выполнения

Рис. 2.3. Влияние удвоения размеров задачи на время выполнения

Нетрудно предсказать влияние удвоения размеров задачи на время выполнения, если время выполнения пропорционально одной из простых функций, указанных в таблице. Теоретически это влияние можно вычислить только для больших N, но данный метод на удивление эффективен. И наоборот, быстрый метод определения функционального роста времени выполнения программы заключается в запуске программы, удвоении объема входных данных для максимально возможного N, а затем оценка функциональной зависимости согласно приведенной таблице.

 Ограничение функции с помощью О-аппроксимации

Рис. 2.4. Ограничение функции с помощью О-аппроксимации

На этой схематической диаграмме осциллирующая кривая представляет собой функцию g(N), которую мы пытаемся аппроксимировать; плавная черная кривая представляет собой другую функцию, f(N), которая используется для аппроксимации, а плавная серая кривая является функцией cf(N) с некоторой неопределенной постоянной c. Вертикальная прямая задает значение N0, указывающее, что аппроксимация справедлива для 
          N > N_0
        . Когда мы говорим, что g(N) = O(f(N)), мы лишь ожидаем, что значение функции g(N) находится ниже некоторой кривой, имеющей форму функции f(N), и правее некоторой вертикальной прямой. Поведение функции f(N) может быть любым (например, она не обязательно должна быть непрерывной).

 Аппроксимация функций

Рис. 2.5. Аппроксимация функций

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

Упражнения

$\triangleright$ 2.20. Докажите, что О(1) - то же самое, что и О(2).

2.21. Докажите, что в выражениях с О-нотацией можно выполнить любое из перечисленных преобразований:


          \begin{align*}
          f(N)&\rightarrow O(f(N))\\
          cO(f(N))&\rightarrow O(f(N))\\
          O(cf(N))&\rightarrow O(f(N))\\
          f(N)-g(N)=O(h(N))&\rightarrow f(N)=g(N)+O(h(N))\\
          O(f(N))O(g(N))&\rightarrow O(f(N)g(N))\\
          O(f(N))+O(g(N))&\rightarrow O(g(N))\\
          \end{align*}

  • 2.22. Покажите, что (N + 1)(H_N + O(1)) = N lnN + O(N).
  • 2.23. Покажите, что 
          N lnN = O(N^{3/2})
        .
  • 2.24. Покажите, что $N^{M}=O(\alpha^{N})$ для любого M и любого постоянного $\alpha >1$.
  • 2.25. Докажите, что $\dfrac{N}{N+O(1)}=1+O\left(\dfrac{1}{N} \right)$
  • 2.26. Предположим, что 
          H_k = N
        . Найдите приближенную формулу, которая выражает k как функцию N.
  • 2.27. Предположим, что lg(k!) = N. Найдите приближенную формулу, которая выражает k как функцию N.
  • 2.28. Известно, что время выполнения одного алгоритма равно O(N logN), а другого - 
          O(N^3)
        . Что это неявно говорит об относительной производительности алгоритмов?
  • 2.29. Известно, что время выполнения одного алгоритма всегда порядка NlogN, а другого - 
          O(N^3)
        . Что это неявно говорит об относительной производительности алгоритмов?
  • 2.30. Известно, что время выполнения одного алгоритма всегда порядка NlogN, а другого - всегда N^3. Что это неявно говорит об относительной производительности алгоритмов?
  • 2.31. Известно, что время выполнения одного алгоритма всегда пропорционально N logN, а другого - всегда пропорционально N^3. Что это неявно говорит об относительной производительности алгоритмов?
  • 2.32. Выведите значения множителей, приведенных на рис. 2.3: для каждой функции f (N), показанной слева, найдите асимптотическую формулу для f (2N) / f (N).
Никита Андриянов
Никита Андриянов
Дмитрий Уколов
Дмитрий Уколов
Семен Дядькин
Семен Дядькин
Беларусь, Минск, БГУ, 2003