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

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

Возрастание функций

Большинство алгоритмов имеют главный параметр N, который наиболее сильно влияет на время их выполнения. Параметр N может быть степенью полинома, размером файла при сортировке или поиске, количеством символов в строке или некоторой другой абстрактной мерой размера рассматриваемой задачи: чаще всего он прямо пропорционален объему обрабатываемого набора данных. Когда таких параметров существует более одного (например, M и N в алгоритмах объединение-поиск, которые были рассмотрены в разделе 1.3 "Введение" ), мы часто сводим анализ к одному параметру, задавая его как функцию других, или рассматривая одновременно только один параметр (считая остальные постоянными) - то есть без потери общности ограничиваясь рассмотрением только одного параметра N. Нашей целью является выражение требований программ к ресурсам (как правило, это время выполнения) в зависимости от N с использованием математических формул, которые максимально просты и справедливы для больших значений параметров. Алгоритмы в этой книге обычно имеют время выполнения, пропорциональное одной из следующих функций:

1 Большинство инструкций большинства программ выполняются один или несколько раз. Если все инструкции программы обладают таким свойством, мы говорим, что время выполнения программы постоянно.
 logN Когда время выполнения программы является логарифмическим, программа выполняется несколько медленнее с ростом N. Такое время выполнения обычно присуще программам, которые сводят большую задачу к набору меньших задач, уменьшая на каждом шаге размер задачи в некоторое постоянное количество раз. В интересующей нас области время выполнения можно считать небольшой константой. Основание логарифма изменяет константу, но не намного: когда N - тысяча,  logN равно 3, если основание равно 10, или порядка 10, если основание равно 2; когда N равно миллиону, значения  logN только удвоятся. При удвоении N величина  logN увеличивается на постоянную величину, а удваивается лишь тогда, когда N достигает N^2.
N Когда время выполнения программы является линейным, это обычно значит, что каждый входной элемент подвергается небольшой обработке. Если N равно миллиону, то время выполнения равно некоторой величине. Когда N удваивается, то же происходит и со временем выполнения. Эта ситуация оптимальна для алгоритма, который должен обработать N входных данных (или выдать N выходных данных).
NlogN Время выполнения, пропорциональное N logN, возникает тогда, когда алгоритм решает задачу, разбивая ее на меньшие подзадачи, решая их независимо и затем объединяя решения. Из-за отсутствия подходящего прилагательного ("линерифмический"?) мы просто говорим, что время выполнения такого алгоритма равно N logN. Если N равно 1 миллиону, N logN примерно равно 20 миллионам. При удвоении N время выполнения более чем (но не сильно) удваивается.
N^2 Если время выполнения алгоритма является квадратичным, он полезен для практического использования для относительно небольших задач. Квадратичное время выполнения обычно появляется в алгоритмах, которые обрабатывают все пары элементов данных (возможно, в цикле двойного уровня вложенности). Когда N равно 1 тысяче, время выполнения равно 1 миллиону. При удвоении N время выполнения увеличивается вчетверо.
 N^3 Аналогично, эта ситуация характерна для алгоритма, который обрабатывает тройки элементов данных (возможно, в цикле тройного уровня вложенности), имеет кубическое время выполнения и практически применим лишь для малых задач. Если N равно 100, время выполнения равно 1 миллиону. При удвоении N время выполнения увеличивается в восемь раз.
  2^N Лишь несколько алгоритмов с экспоненциальным временем выполнения имеют практическое применение, хотя такие алгоритмы возникают естественным образом при попытках прямого решения задачи. Если N равно 20, время выполнения равно 1 миллиону. При удвоении N время выполнения возводится в квадрат!

Время выполнения определенной программы обычно равно некоторой константе, умноженной на один из этих элементов (главный член) плюс меньшие слагаемые. Значения постоянного коэффициента и остальных слагаемых зависят от результатов анализа и деталей реализации. В первом приближении коэффициент при главном члене связан с количеством инструкций во внутреннем цикле: на любом уровне разработки алгоритма разумно сократить количество таких инструкций. Для больших N доминирует эффект главного члена, для малых N или для тщательно разработанных алгоритмов ощутимый вклад дают и другие слагаемые, поэтому сравнение алгоритмов затрудняется. В большинстве случаев мы будем называть время выполнения программ просто "линейным", "N logN", "кубическим" и т.д. Обоснование этого подробно приводится в разделе 2.4.

В итоге, чтобы уменьшить общее время выполнения программы, мы минимизируем количество инструкций во внутреннем цикле. Каждую инструкцию необходимо тщательно рассмотреть и решить, является ли она необходимой. Существует ли более эффективный способ выполнить такую же задачу? Некоторые программисты считают, что средства автоматизации, содержащиеся в современных компиляторах, могут создавать наилучший машинный код; другие утверждают, что оптимальным способом является написание внутренних циклов вручную на машинном языке, или ассемблере. Обычно мы будем останавливаться, доходя до рассмотрения оптимизации на таком уровне, хотя иногда будем указывать, сколько машинных инструкций требуется для выполнения определенных операций. Это потребуется для того, чтобы понять, почему на практике одни алгоритмы могут оказаться быстрее других.

Для малых задач время решения практически не зависит от метода - быстрый современный компьютер все равно выполнит задачу мгновенно. Но по мере увеличения размера задачи числа, с которыми мы имеем дело, становятся огромными, как продемонстрировано в таблица 2.1. Когда количество исполняемых инструкций в медленном алгоритме становится по-настоящему большим, время, необходимое для их выполнения, становится недостижимым даже для самых быстрых компьютеров. На рис. 2.1 приведен перевод большого количества секунд в дни, месяцы, годы и т.д.; в таблица 2.2 показаны примеры того, как быстрые алгоритмы имеют больше возможностей решить задачу за приемлемое время, нежели даже самые быстрые компьютеры.

 Перевод секунд

Рис. 2.1. Перевод секунд

Огромная разница между такими числами, как 10^4 и 10^8, становится более очевидной, если взять соответствующее количество секунд и перевести в привычные единицы измерения. Мы можем позволить программе выполняться 2,8 часа, но вряд ли мы будем созерцать программу, выполнение которой займет 3,1 года. Поскольку 2^10 примерно равно 10^3, этой таблицей можно воспользоваться и для перевода степеней 2. Например, 2^{32} секунд - это примерно 124 года.

Таблица 2.1. Значения часто встречающихся функций
lgN $\sqrt{N}$ N NlgN 
              N(lgN)^2
             N^{3/2} N^2
3 3 10 33 110 32 100
7 10 100 664 4414 1000 10000
10 32 1000 9966 99317 31623 1000000
13 100 10000 132877 1765633 1000000 100000000
17 316 100000 1660964 27588016 31622777 10000000000
20 1000 1000000 19931569 397267426 1000000000 1000000000000

В этой таблице показаны относительные величины некоторых функций, которые часто встречаются при анализе алгоритмов. Квадратичная функция очевидно доминирует, особенно для больших значений N, а различия между меньшими функциями оказываются не такими, как можно было ожидать для малых N. Например, N^{3/2} должно быть больше, чем 
          Nlg^2
        для очень больших значений N, однако для малых N наблюдается обратная ситуация. Точное время выполнения алгоритма может быть линейной комбинацией этих функций. Быстрые алгоритмы легко отличить от медленных из-за огромной разницы между, например, lgN и N или N и N^2, но различие между двумя быстрыми алгоритмами может потребовать тщательного изучения.

Таблица 2.2. Время для решения гигантских задач
Операций в секунду Размер задачи 1 миллион Размер задачи 1 миллиард
N NlgN  N^2 N NlgN  N^2
10^6 секунды секунды недели часы часы никогда
10^9 мгновенно мгновенно часы секунды секунды десятилетия
10^{12} мгновенно мгновенно секунды мгновенно мгновенно недели

Во многих случаях единственным шансом решить очень большую задачу является использование эффективного алгоритма. В этой таблице показано минимальное количество времени, необходимое для решения задач размером 1 миллион и 1 миллиард с использованием линейных, Nlog N и квадратичных алгоритмов на компьютерах с быстродействием 1 миллион, 1 миллиард и 1 триллион инструкций в секунду. Быстрый алгоритм позволяет решить задачу на медленной машине, но быстрая машина бессильна при использовании медленного алгоритма.

При анализе алгоритмов возникает еще несколько функций. Например, алгоритм с N^2 входными данными, имеющий время выполнения N^3, можно рассматривать, как N^{3/2} алгоритм. Кроме того, некоторые алгоритмы разбиваются на подзадачи в два этапа и имеют время выполнения, пропорциональное Nlog^2. Из таблица 2.1 видно, что обе эти функции гораздо ближе к NlogN, чем N^2.

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

В математике настолько важным является натуральный логарифм (основание e = 2,71828...), что распространено следующее сокращение: $\log{_{e}N}\equiv\ln{N}$. В вычислительной технике очень важен двоичный логарифм (основание равно 2), поэтому часто используется сокращение $\log{_{2}N}\equiv\lg{N}$.

Наименьшее целое число, большее lgN, равно количеству битов, необходимых для представления N в двоичном формате; точно так же наименьшее целое, большее $\log{_{10}N}$, - это количество цифр, необходимое для представления N в десятичном формате.

Оператор С++

          for (lgN = 0; N > 0; lgN++, N /= 2) ;
      

дает простой способ подсчета наименьшего целого, большего lgN. Вот аналогичный метод для вычисления этой функции:

        for (lgN = 0, t = 1; t < N; lgN++, t += t) ;
      

В нем подчеркивается, что ${2^{n}}\leq{N}<{2^{n+1}}$, когда n - это наименьшее целое, большее lgN.

Иногда бывает нужно вычислить логарифм логарифма, обычно для больших чисел. Например, 
          lglg2^{256} = lg 256 = 8
        . Как видно из данного примера, обычно для практических целей выражение loglog N можно считать константой, поскольку оно мало даже для очень больших N.

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

Таблица 2.3. Специальные функции и постоянные
Функция Название Пример Приближение
$\lfloor{x}\rfloor$ округление до меньшего $\lfloor{3,14}\rfloor$= 3 x
$\lceil{x}\rceil$ округление до большего $\lceil{3,14}\rceil$= 4 x
lg N двоичный логарифм lg1024 =10 1,44lnN
F^n числа Фибоначчи 
             F^10 = 55
            $\phi^{N}/\sqrt{5}$
H^N гармонические числа  $H_{10}\approx2,9$ $\ln{N}+\gamma$
N! факториал 10! = 3628800 
              (N/e)^N
lg (N!) $\lg{100!}\approx520$ 
              N lgN - 1,44 N

e = 2,71828...

$\gamma = 0,57721...$

$\phi=(1+\sqrt{5})/2=1,61803...$

ln2 = 0,693147...

 lge = 1/ln 2 = 1,44269...

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

Никита Андриянов
Никита Андриянов
Дмитрий Уколов
Дмитрий Уколов
Индира Махамбаева
Индира Махамбаева
Казахстан, г.Кызылорда
Елена Фомина
Елена Фомина
Россия, Москва, 45