Вам бонус- начислено 1 монета за дневную активность. Сейчас у вас 1 монета

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

Лекция



Это продолжение увлекательной статьи про сложность алгоритмов.

...

строгая

  • Здесь O-сложность большего масштаба, чем Θ, поэтому эта граница нестрогая. В самом деле, строгой границей здесь будет O( n2 ). Так что мы можем записать, что алгоритм является o( n3 )
  • Снова, O-сложность большего масштаба, чем Θ, из чего мы заключаем, что граница нестрогая. Строгой будет O( 1 ), а O( n )можно переписать, как o( n )
  • Мы должны были совершить ошибку при выводе этой границы, потому что она неверна. Об этом говорит сайт https://intellect.icu . Θ( n ) алгоритм не может иметь верхнюю границу O( 1 ), поскольку n имеет большую сложность, чем 1. Не забывайте, O обозначает верхнюю границу
  • Может показаться, что тут нестрогая граница, но, вообще-то, это не так. На самом деле, граница строгая. Напомню, что асимптотики2n и n — одинаковые, а O и Θ связаны только с асимптотиками. Так что мы имеем O( 2n ) = O( n ), следовательно, граница строгая, поскольку сложность равна Θ


  • Практическая рекомендация: выяснить O-сложность алгоритма проще, чем его Θ-сложность.

    К настоящему моменты вы могли уже порядком запутаться во всех этих новых обозначениях, но давайте познакомимся с еще двумя символами перед тем, как перейти к дальнейшим примерам. Выше мы меняли нашу программу, чтобы сделать ее хуже (увеличили количество инструкций и, таким образом, время выполнения), чем создали O-нотацию. О говорит нам о том, что наш код никогда не будет работать медленнее определенного предела. Из этого мы получаем основания для оценки: достаточно ли хороша наша программа? Если же мы поступим противоположным образом, сделав имеющийся код лучше, и найдем сложность того, что получится, то мы задействуем Ω-нотацию. Таким образом, Ω дает нам сложность, лучше которой наша программа быть не может. Она полезна, если мы хотим доказать, что программа работает медленно или алгоритм является плохим. Так же ее можно применить, когда мы утверждаем, что алгоритм слишком медленный для использования в данном конкретном случае. Например, высказывание, что алгоритм является Ω( n3 ), означает, что алгоритм не лучше, чемn3. Он может быть Θ( n3 ), или Θ( n4 ), или еще хуже, но мы будем знать предел его «хорошести». Таким образом, Ω дает нам нижнюю границу сложности нашего алгоритма. Аналогично ο, мы можем писать ω, если знаем, что этот предел нестрогий. Например, Θ( n3 ) алгоритм является ο( n4 ) и ω( n2 ). Ω( n ) произносится как «большое омега от n», в то время как ω( n ) произносится «маленькое омега от n».
    Упражнение 6
    Для следующих Θ-сложностей напишите строгие и нестрогие О-пределы и, по желанию, строгие и нестрогие Ω-пределы (при условии, что они существуют).
    1. Θ( 1 )
    2. Θ( √n )
    3. Θ( n )
    4. Θ( n2 )
    5. Θ( n3 )
    Решение
    Это упражнение на прямое использование определения выше.
    1. Строгие границы будут O( 1 ) и Ω( 1 ). Нестрогой О-границей будет O( n ). Напоминаю, что О дает нам верхний предел. Поскольку n лежит на шкале выше, чем 1, то это — нестрогий предел, и мы также можем записать его как o( n ). А вот найти нестрогий предел для Ω мы не можем, потому что не существует функции ниже, чем 1. Так что придется иметь дело со строгой границей
    2. Строгие пределы будут теми же, что и Θ-сложность, т.е. O( √n ) и Ω( √n ) соответственно. Для нестрогих пределов будем иметь O( n ), поскольку n больше, чем √n. А поскольку эта граница нестрогая, то можем написать o( n ). В качестве нижней нестрогой границы мы попросту используем Ω( 1 ) (или ω( 1 ))
    3. Строгие пределы O( n ) и Ω( n ). Нестрогими могут быть ω( 1 ) и o( n3 ). Не самые лучшие границы, поскольку обе достаточно далеки от оригинальной сложности, но они подходят под наше определение
    4. Строгие границы O( n2 ) и Ω( n2 ). В качестве нестрогих границ мы используем ω( 1 ) и o( n3 ), как и в предыдущем примере
    5. Строгие границы O( n3 ) и Ω( n3 ), соответственно. Двумя нестрогими границами могут быть ω( √n n2 ) и o( √n n3 ). Хотя эти пределы по-прежнему нестрогие, но они лучше тех, что мы выводили выше


    Причина, по которой мы используем O и Ω вместо Θ, в том, что мы можем быть не уверены в точности найденных нами границ или просто не хотим копать алгоритм глубже.

    Если вы не до конца запомнили все это многообразие обозначений, не стоит переживать. Вы всегда можете вернуться и освежить в памяти информацию по ним. Наиболее важные символы — это O и Θ.

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

    В следующей таблице собраны символы, которые мы представили выше, и их связь с обычными математическими значками для сравнения чисел. Причина, по которой мы пользуемся греческими буквами вместо привычной математической нотации, в необходимости показать, что мы имеем дело со сравнением асимптотических оценок, а не с обычным.
    Оператор сравнения асимптотических оценок Оператор сравнения чисел
    Алгоритм является o( что-то ) Число < чего-то
    Алгоритм является O( что-то ) Число чего-то
    Алгоритм является Θ( что-то ) Число = чему-то
    Алгоритм является Ω( что-то ) Число чего-то
    Алгоритм является ω( что-то ) Число > чего-то


    Практическая рекомендация: несмотря на то, что все символы O, o, Ω, ω и Θ необходимы время от времени, O используется чаще всего, поскольку его проще найти, чем Θ, и оно более полезно на практике, чем Ω.

    Логарифмы


    Анализ и оценка сложности алгоритмов, О большое, функции оценки сложности
    Если вы знаете, что такое логарифмы, то можете спокойно пропустить этот раздел. Глава предназначается тем, кто незнаком с данным понятием или пользуется им настолько редко, что уже забыл что там к чему. Логарифмы важны, поскольку они очень часто встречаются при анализе сложности. Логарифм— это операция, которая при применении ее к числу делает его гораздо меньше (подобно взятию квадратного корня). Итак, первая вещь, которую вы должны запомнить: логарифм возвращает число, меньшее, чем оригинал. На рисунке справа зеленый график — линейная функция f(n) = n, красный — f(n) = sqrt(n), а наименее быстро возрастающий — f(n) = log(n). Далее: подобно тому, как взятие квадратного корня является операцией, обратной возведению в квадрат, логарифм — обратная операция возведению чего-либо в степень.

    Поясню на примере. Рассмотрим уравнение

    2x = 1024

    Чтобы найти из него x, спросим себя: в какую степень надо возвести 2, чтобы получить 1024? Ответ: в десятую. В самом деле, 210 = 1024, что легко проверить. Логарифмы помогают нам описать данную задачу, используя новую нотацию. В данном случае, 10 является логарифмом 1024 и записывается, как log( 1024 ). Читается: «логарифм 1024». Поскольку мы использовали 2 в качестве основания, то такие логарифмы называются «по основанию 2». Основанием может быть любое число, но в этой статье мы будем использовать только двойку. Если вы ученик-олимпиадник и не знакомы с логарифмами, то я рекомендую вам попрактиковаться после того, как закончите чтение этой статьи. В информатике логарифмы по основанию 2 распространены больше, чем какие-либо другие, поскольку часто мы имеем всего две сущности: 0 и 1. Также существует тенденция разбивать объемные задачи пополам, а половин, как известно, тоже бывает всего две. Поэтому для дальнейшего чтения статьи вам достаточно иметь представление только о логарифмах по основанию 2.
    Упражнение 7
    Решите уравнения ниже, записывая логарифмы, которые вы ищете. Используйте только логарифмы по основанию 2.
    1. 2x = 64
    2. (22)x = 64
    3. 4x = 4
    4. 2x = 1
    5. 2x + 2x = 32
    6. (2x) * (2x) = 64
    Решение
    Здесь не нужно ничего сверх идей, изложенных выше.
    1. Методом проб и ошибок, находим, что x = 6. Таким образом, log( 64 ) = 6
    2. По свойству степеней (22)x можно записать как 22x. Таким образом, получим, что 2x = 6 (потому что log( 64 ) = 6 из предыдущего примера) и, следовательно, x = 3
    3. Используя знания, полученные при решении предыдущего примера, запишем 4 как 22. Тогда наше уравнение превратится в (22)x = 4, что эквивалентно 22x. Заметим, что log( 4 ) = 2 (поскольку 22 = 4), так что мы получаем 2x = 2, откуда x = 1. Это легко заметить по исходному уравнению, поскольку степень 1 дает основание в качестве результата
    4. Вспомнив, что степень 0 дает результатом 1 (т.е. log( 1 ) = 0), получим x = 0
    5. Здесь мы имеем дело с суммой, поэтому непосредственно взять логарифм не получится. Однако, мы замечаем, что 2x + 2x — это тоже самое, что и 2 * (2x). Умножение на 2 даст 2x + 1, и мы получим уравнение 2x + 1 = 32. Находим, что log( 32 ) = 5, откудаx + 1 = 5 и x = 4.
    6. Мы перемножаем две степени двойки, следовательно, можем объединить их в 22x. Остается просто решить уравнение 22x = 64, что мы уже делали выше. Ответ: x = 3


    Практическая рекомендация: на соревнованиях алгоритмы часто реализуются на С++. Как только вы проанализировали сложность вашего алгоритма, так сразу можете получить и грубую оценку того, как быстро он будет работать, приняв, что в секунду выполняется 1 000 000 команд. Их количество считается из полученной вами функции асимптотической оценки, описывающей алгоритм. Например, вычисление по алгоритму с Θ( n ) займет около секунды при n = 1 000 000.

    рекурсивная сложность


    А теперь давайте обратимся к рекурсивным функциям. Рекурсивная функция — это функция, которая вызывает сама себя. Можем ли мы проанализировать ее сложность? Следующая функция, написанная на Python, вычисляет факториал заданного числа. Факториал целого положительного числа находится как произведение всех предыдущих положительных целых чисел. Например, факториал 5 — это 5 * 4 * 3 * 2 * 1. Обозначается он как «5!» и произносится «факториал пяти» (впрочем, некоторые предпочитают «ПЯТЬ!!!11»).
    def factorial( n ):
        if n == 1:
            return 1
        return n * factorial( n - 1 )
    

    Давайте проанализируем эту функцию. Она не содержит циклов, но ее сложность все равно не является константной. Что ж, придется вновь заняться подсчетом инструкций. Очевидно, что если мы применим эту функцию к некоторому n, то она будет вычисляться n раз. (Если вы в этом не уверены, то можете вручную расписать ход вычисления при n = 5, чтобы определить, как это на самом деле работает.) Таким образом, мы видим, что эта функция является Θ( n ).

    Если вы все же не уверены в этом, то вы всегда можете найти точную сложность путем подсчета количества инструкций. Примените этот метод к данной функции, чтобы найти ее f( n ), и убедитесь, что она линейная (напомню, что линейность означает Θ( n )).

    логарифмическая сложность


    Одной из известнейших задач в информатике является поиск значения в массиве. Мы уже решали ее ранее для общего случая. Задача становится интереснее, если у нас есть отсортированный массив, в котором мы хотим найти заданное значение. Одним из способов сделать это является бинарный поиск. Мы берем средний элемент из нашего массива: если он совпадает с тем, что мы искали, то задача решена. В противном случае, если заданное значение больше этого элемента, то мы знаем, что оно должно лежать в правой части массива. А если меньше — то в левой. Мы будем разбивать эти подмассивы до тех пор, пока не получим искомое.
    Анализ и оценка сложности алгоритмов, О большое, функции оценки сложности
    Вот реализация такого метода в псевдокоде:
     Анализ и оценка сложности алгоритмов, О большое, функции оценки сложности
    

    Анализ и оценка сложности алгоритмов, О большое, функции оценки сложности


    Приведенный псевдокод — упрощение настоящей реализации. На практике этот метод описать проще, чем воплотить, поскольку программисту нужно решить некоторые дополнительные вопросы. Например, защиту от ошибок на одну позицию (off-by-one error, OBOE), да и деление на два не всегда может давать целое число, поэтому потребуется применять функции floor() или ceil(). Однако, предположим, что в нашем случае деление всегда будет успешным, наш код защищен от ошибок off-by-one, и все, что мы хотим — это проанализировать сложность данного метода. Если вы никогда раньше не реализовывали бинарный поиск, то можете сделать это на вашем любимом языке программирования. Такая задача по-настоящему поучительна.

    Если вы не уверены, что метод работает в принципе, то отвлекитесь и решите вручную какой-нибудь простой пример.

    Теперь давайте попробуем проанализировать этот алгоритм. Еще раз, в этом случае мы имеем рекурсивный алгоритм. Давайте для простоты предположим, что массив всегда разбивается ровно пополам, игнорируя +1 и -1 части в рекурсивном вызове. К этому моменту вы должны быть уверены, что такое небольшое изменение, как игнорирование +1 и -1, не повлияет на конечный результат оценки сложности. В принципе, обычно этот факт необходимо доказывать, если мы хотим проявить осторожность с математической точки зрения. Но на практике это очевидно на уровне интуиции. Также для простоты давайте предположим, что наш массив имеет размер одной из степеней двойки. Опять-таки, это предположение никоим образом не повлияет на конечный результат расчета сложности алгоритма. Наихудшим сценарием для данной задачи будет вариант, когда массив в принципе не содержит искомое значение. В этом случае мы начинаем с массива размером n на первом рекурсивном вызове, n / 2 на втором, n / 4 на третьем и так далее. В общем, наш массив разбивается пополам на каждом вызове до тех пор, пока мы не достигнем единицы. Давайте запишем количество элементов в массиве на каждом вызове:
    0-я итерация: n
    1-я итерация: n / 2
    2-я итерация: n / 4
    3-я итерация: n / 8

    i-я итерация: n / 2i

    последняя итерация: 1

    Заметьте, что на i-й итерации массив имеет n / 2i элементов. Мы каждый раз разбиваем его пополам, что подразумевает деление количества элементов на два (равноценно умножению знаменателя на два). Проделав это i раз, получим n / 2i. Процесс будет продолжаться, и из каждого большого i мы будем получать меньшее количество элементов до тех пор, пока не достигнем единицы. Если мы захотим узнать, на какой итерации это произошло, нам нужно будет просто решить следующее уравнение:

    1 = n / 2i

    Равенство будет истинным только тогда, когда мы достигнем конечного вызова функции binarySearch(), так что узнав из него i, мы будем знать номер последней рекурсивной итерации. Умножив обе части на 2i, получим:

    2i = n

    Если вы прочли раздел о логарифмах выше, то такое выражение будет для вас знакомым. Решив его, мы получим:

    i = log( n )

    Этот ответ говорит нам, что количество итераций, необходимых для бинарного поиска, равняется log( n ), где n — размер оригинального массива.

    Если немного подумать, то это имеет смысл. Например, возьмем n = 32 (массив из 32-х элементов). Сколько раз нам потребуется разделить его, чтобы получить один элемент? Считаем: 32 → 16 → 8 → 4 → 2 → 1 — итого 5 раз, что является логарифмом 32. Таким образом, сложность бинарного поиска равна Θ( log( n ) ).

    Последний результат позволяет нам сравнивать бинарный поиск с линейным (нашим предыдущим методом). Несомненно, log( n ) намного меньше n, из чего правомерно заключить, что первый намного быстрее второго. Так что имеет смысл хранить массивы в отсортированном виде, если мы собираемся часто искать в них значения.

    Практическая рекомендация: улучшение асимптотического времени выполнения программы часто чрезвычайно повышает ее производительность. Намного сильнее, чем небольшая «техническая» оптимизация в виде использования более быстрого языка программирования.

    Оптимальная сортировка


    Поздравляю! Теперь вы знаете о том, как анализировать сложность алгоритмов , что такое асимптотическая оценка и нотация «большое-О». Вы также в курсе, как интуитивно выяснить является ли сложностью алгоритма O( 1 ), O( log( n ) ), O( n ), O( n2 ) и так далее. Вы знакомы с символами o, O, ω, Ω, Θ и понятием «наихудшего случая». Если вы добрались до этого места, то моя статья уже выполнила свою задачу.

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

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

    Прежде, чем реализовать собственно сортировку слиянием, нам надо написать вспомогательную функцию. Назовем ее merge, и пусть она принимает два уже отсортированных массива и соединяет их в один (тоже отсортированный). Это легко сделать:
    def merge( A, B ):
        if empty( A ):
            return B
        if empty( B ):
            return A
        if A[ 0 ] < B[ 0 ]:
            return concat( A[ 0 ], merge( A[ 1...A_n ], B ) )
        else:
            return concat( B[ 0 ], merge( A, B[ 1...B_n ] ) )
    

    Функция concat принимает элемент («голову») и массив («хвост») и соединяет их, возвращая новый массив с «головой» в качестве первого элемента и «хвостом» в качестве оставшихся элементов. Например, concat( 3, [ 4, 5, 6 ] ) вернет [ 3, 4, 5, 6 ]. Мы используем A_n иB_n, чтобы обозначить размеры массивов A и B соответственно.
    Упражнение 8
    Убедитесь, что функция выше действительно осуществляет слияние. Перепишите ее на вашем любимом языке программирования, используя итерации (цикл for) вместо рекурсии.


    Анализ этого алгоритма показывает, что его время выполнения — Θ( n ), где n — длина результирующего массива (n = A_n + B_n).
    Упражнение 9
    Проверьте, что временем выполнения merge является Θ( n ).


    Используя эту функцию, мы можем построить лучший алгоритм сортировки. Идея следующая: мы разбиваем массив на две части, рекурсивно сортируем каждую из них и объединяем два отсортированных массива в один. Псевдокод:
    def mergeSort( A, n ):
        if n = 1:
            return A # it is already sorted
        middle = floor( n / 2 )
        leftHalf = A[ 1...middle ]
        rightHalf = A[ ( middle + 1 )...n ]
        return merge( mergeSort( leftHalf, middle ), mergeSort( rightHalf, n - middle ) )
    

    Эта функция сложнее предыдущих для понимания, поэтому выполнение следующего упражнения может потребовать у вас больше времени.
    Упражнение 10
    Убедитесь в правильности mergeSort (т.е. проверьте, что она верно сортирует переданный ей массив). Если вы не можете понять, почему она в принципе работает, то прорешайте вручную небольшой пример. При этом не забудьте убедиться, что leftHalf и rightHalf получаются при разбиении массива примерно пополам. Разбиение не обязательно должно приходиться точно на середину, если, например, массив содержит нечетное количество элементов (вот для чего нам нужна функция floor).


    В качестве финального упражнения давайте проанализируем сложность mergeSort. На каждом шаге мы разбиваем исходный массив на две равные части (аналогично binarySearch). Однако, в этом случае дальнейшие вычисления происходят на обеих частях. После возврата рекурсии мы применяем к результатам операцию merge, что занимает Θ( n ) времени.

    Итак, мы разбили оригинальный массив на две части, размером n / 2 каждая. Когда мы будем их соединять, в операции будут участвовать nэлементов, следовательно, время выполнения будет Θ( n ). Рисунок ниже поясняет эту рекурсию:

    Анализ и оценка сложности алгоритмов, О большое, функции оценки сложности

    Давайте посмотрим, что здесь происходит. Каждый кружок представляет собой вызов функции mergeSort. Число в кружке показывает количество элементов в массиве, который будет отсортирован. Верхний синий кружок — первоначальный вызов mergeSort для массива, размером n. Стрелочки показывают рекурсивные вызовы между функциями. Оригинальный вызов mergeSort порождает два новых для массивов, размером n / 2 каждый. В свою очередь, каждый из них порождает еще два вызова с массивами в n / 4 элементов, и так далее до тех пор, пока мы не получим массив размера 1. Эта диаграмма называется рекурсивным деревом, потому что иллюстрирует работу рекурсии и выглядит как дерево (точнее, как инверсия дерева с корнем вверху и листьями внизу).

    Заметьте, что количество элементов в каждой строке остается равным n. Так же обратите внимание, что каждый вызывающий узел использует для операции merge результаты, полученные от вызываемых узлов. Например, красный узел сортирует n / 2 элементов. Чтобы сделать это, он разбивает n / 2 массив на два по n / 4, рекурсивно вызывает с каждым из них mergeSort (зеленые узлы) и объединяет результаты в единое целое размером n / 2.

    В результате сложностью каждой строки является Θ( n ). Мы знаем, что количество таких строк, называемое глубиной рекурсивного дерева, будет log( n ). Основанием для этого являются те же аргументы, которые мы использовали для бинарного поиска. Итак, у нас log( n ) строк со сложностью Θ( n ), следовательно, общая сложность mergeSort: Θ( n * log( n ) ). Это намного лучше, чем Θ( n2 ), которую нам дает сортировка выбором (помните, log( n ) намного меньше n, поэтому n * log( n ) так же намного меньше n * n = n2).

    Как вы видели в последнем примере, анализ сложности позволяет нам сравнивать алгоритмы, чтобы понять, который из них лучше. Исходя из этих соображений, теперь мы можем быть уверены, что для массивов больших размеров сортировка слиянием намного опередит сортировку выбором. Такое заключение было бы проблематично сделать, если бы мы не имели теоретического базиса анализа разработанных нами алгоритмов. Несомненно, алгоритмы сортировки с временем выполнения Θ( n * log( n ) ) широко используются на практике. Например, ядро Linux использует алгоритм, называемый «сортировкой кучей», который имеет такое же время выполнения, как и сортировка слиянием. Заметим, что мы не будем доказывать оптимальность данных алгоритмов сортировки. Это потребовало бы гораздо более громоздких математических аргументов, но будьте уверены: с точки зрения сложности нельзя найти вариант лучше.

    После изучения данной статьи, приобретенная вами интуиция относительно анализа сложности алгоритмов должна будет помогать вам создавать быстрые программы и сосредотачивать усилия по оптимизации на тех вещах, которые действительно имеют большое влияние на скорость выполнения. Все вместе позволит вам работать более продуктивно. К тому же математический язык и нотации (например, «большое О»), рассмотренные в данной статье, будут вам полезны при общении с другими разработчиками программного обеспечения, когда речь зайдет о времени выполнения алгоритмов, и, надеюсь, вы сможете применить на практике приобретенные знания.

    оценка сложности алгоритмов

    Существует несколько способов измерения сложности алгоритма. Программисты обычно сосредотачивают внимание на скорости алгоритма, но не менее важны и другие показатели – требования к объему памяти, свободному месте на диске. Использование быстрого алгоритма не приведет к ожидаемым результатам, если для его работы понадобится больше памяти, чем есть у компьютера.

    Память или время


    Многие алгоритмы предлагают выбор между объемом памяти и скоростью. Задачу можно решить быстро, использую большой объем памяти, или медленнее, занимая меньший объем.
    Типичным примером в данном случае служит алгоритм поиска кратчайшего пути. Представив кару города в виде сети, можно написать алгоритм для определения кратчайшего расстояния между двумя любыми точками этой сети. Чтобы не вычислять эти расстояния всякий раз, когда они нам нужны, мы можем вывести кратчайшие расстояния между всеми точками и сохранить результаты в таблице. Когда нам понадобится узнать кратчайшее расстояние между двумя заданными точками, мы можем просто взять готовое расстояние из таблицы.
    Результат будет получен мгновенно, но это потребует огромного объема памяти. Карта большого города может содержать десятки тысяч точек. Тогда, описанная выше таблица, должна содержать более 10 млрд. ячеек. Т.е. для того, чтобы повысить быстродействие алгоритма, необходимо использовать дополнительные 10 Гб памяти.
    Из этой зависимости проистекает идея объемно-временной сложности. При таком подходе алгоритм оценивается, как с точки зрении скорости выполнения, так и с точки зрения потребленной памяти.
    Мы будем уделять основное внимание временной сложности, но, тем не менее, обязательно будем оговаривать и объем потребляемой памяти.

    Оценка порядка


    При сравнении различных алгоритмов важно знать, как их сложность зависит от объема входных данных. Допустим, при сортировке одним методом обработка тысячи чисел занимает 1 с., а обработка миллиона чисел – 10 с., при использовании другого алгоритма может потребоваться 2 с. и 5 с. соответственно. В таких условиях нельзя однозначно сказать, какой алгоритм лучше.
    В общем случае сложность алгоритма можно оценить по порядку величины. Алгоритм имеет сложность O(f(n)), если при увеличении размерности входных данных N, время выполнения алгоритма возрастает с той же скоростью, что и функция f(N). Рассмотрим код, который для матрицы A[NxN] находит максимальный элемент в каждой строке.


    for i:=1 to N do
    begin
    max:=A[i,1];
    for j:=1 to N do
    begin
    if A[i,j]>max then
    max:=A[i,j]
    end;
    writeln(max);
    end;


    В этом алгоритме переменная i меняется от 1 до N. При каждом изменении i, переменная j тоже меняется от 1 до N. Во время каждой из N итераций внешнего цикла, внутренний цикл тоже выполняется N раз. Общее количество итераций внутреннего цикла равно N*N. Это определяет сложность алгоритма O(N^2).
    Оценивая порядок сложности алгоритма, необходимо использовать только ту часть, которая возрастает быстрее всего. Предположим, что рабочий цикл описывается выражением N^3+N. В таком случае его сложность будет равна O(N^3). Рассмотрение быстро растущей части функции позволяет оценить поведение алгоритма при увеличении N. Например, при N=100, то разница между N^3+N=1000100 и N=1000000 равна всего лишь 100, что составляет 0,01%.
    При вычислении O можно не учитывать постоянные множители в выражениях. Алгоритм с рабочим шагом 3N^3 рассматривается, как O(N^3). Это делает зависимость отношения O(N) от изменения размера задачи более очевидной.

    Определение сложности алгоритма


    Наиболее сложными частями программы обычно является выполнение циклов и вызов процедур. В предыдущем примере весь алгоритм выполнен с помощью двух циклов.
    Если одна процедура вызывает другую, то необходимо более тщательно оценить сложность последней. Если в ней выполняется определенное число инструкций (например, вывод на печать), то на оценку сложности это практически не влияет. Если же в вызываемой процедуре выполняется O(N) шагов, то функция может значительно усложнить алгоритм. Если же процедура вызывается внутри цикла, то влияние может быть намного больше.
    В качестве примера рассмотрим две процедуры: Slow со сложностью O(N^3) и Fast со сложностью O(N^2).


    procedure Slow;
    var
    i,j,k: integer;
    begin
    for i:=1 to N do
    for j:=1 to N do
    for k:=1 to N do
    {какое-то действие}
    end;
    procedure Fast;
    var
    i,j: integer;
    begin
    for i:=1 to N do
    for j:=1 to N do
    Slow;
    end;
    procedure Both;
    begin
    Fast;
    end;


    Если во внутренних циклах процедуры Fast происходит вызов процедуры Slow, то сложности процедур перемножаются. В данном случае сложность алгоритма составляет O(N^2 )*O(N^3 )=O(N^5).
    Если же основная программа вызывает процедуры по очереди, то их сложности складываются: O(N^2 )+O(N^3 )=O(N^3). Следующий фрагмент имеет именно такую сложность:
    procedure Slow;
    var
    i,j,k: integer;
    begin
    for i:=1 to N do
    for j:=1 to N do
    for k:=1 to N do
    {какое-то действие}
    end;
    procedure Fast;
    var
    i,j: integer;
    begin
    for i:=1 to N do
    for j:=1 to N do
    {какое-то действие}
    end;
    procedure Both;
    begin
    Fast;
    Slow;
    end;

    сложность рекурсивных алгоритмов

    Простая рекурсия


    Напомним, что рекурсивными процедурами называются процедуры, которые вызывают сами себя. Их сложность определить довольно тяжело. Сложность этих алгоритмов зависит не только от сложности внутренних циклов, но и от количества итераций рекурсии. Рекурсивная процедура может выглядеть достаточно простой, но она может серьезно усложнить программу, многократно вызывая себя.
    Рассмотрим рекурсивную реализацию вычисления факториала:
    function Factorial(n: Word): integer;
    begin
    if n > 1 then
    Factorial:=n*Factorial(n-1)
    else
    Factorial:=1;
    end;

    Эта процедура выполняется N раз, таким образом, вычислительная сложность этого алгоритма равна O(N).

    Многократная рекурсия


    Рекурсивный алгоритм, который вызывает себя несколько раз, называется многократной рекурсией. Такие процедуры гораздо сложнее анализировать, кроме того, они могут сделать алгоритм гораздо сложнее.
    Рассмотрим такую процедуру:
    procedure DoubleRecursive(N: integer);
    begin
    if N>0 then
    begin
    DoubleRecursive(N-1);
    DoubleRecursive(N-1);
    end;
    end;

    Поскольку процедура вызывается дважды, можно было бы предположить, что ее рабочий цикл будет равен O(2N)=O(N). Но на самом деле ситуация гораздо сложнее. Если внимательно исследовать этот алгоритм, то станет очевидно, что его сложность равна O(2^(N+1)-1)=O(2^N). Всегда надо помнить, что анализ сложности рекурсивных алгоритмов весьма нетривиальная задача.
    Объемная сложность рекурсивных алгоритмов

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

    продолжение следует...

    Продолжение:


    Часть 1 Анализ и оценка сложности алгоритмов, О большое, функции оценки сложности
    Часть 2 Оптимальная сортировка - Анализ и оценка сложности алгоритмов, О большое,
    Часть 3 Примеры - Анализ и оценка сложности алгоритмов, О большое, функции
    Часть 4 Почему О большое может не имеет значения - Анализ и

    См.также

    А как ты думаешь, при улучшении сложность алгоритмов, будет лучше нам? Надеюсь, что теперь ты понял что такое сложность алгоритмов, оценка сложности алгоритмов, анализ сложности алгоритмов, большое o, доминирующий оператор, о большое, big o, рекурсивная сложность, логарифмическая сложность, сложность рекурсивных алгоритмов, функции оценки сложности, практическая оценка сложности алгоритмов и для чего все это нужно, а если не понял, или есть замечания, то не стесняйся, пиши или спрашивай в комментариях, с удовольствием отвечу. Для того чтобы глубже понять настоятельно рекомендую изучить всю информацию из категории Алгоритмы и теория алгоритмов

    Ответы на вопросы для самопроверки пишите в комментариях, мы проверим, или же задавайте свой вопрос по данной теме.

    создано: 2014-08-18
    обновлено: 2024-11-13
    1757



    Рейтиг 2 of 10. count vote: 2
    Вы довольны ?:


    Поделиться:

    Найди готовое или заработай

    С нашими удобными сервисами без комиссии*

    Как это работает? | Узнать цену?

    Найти исполнителя
    $0 / весь год.
    • У вас есть задание, но нет времени его делать
    • Вы хотите найти профессионала для выплнения задания
    • Возможно примерение функции гаранта на сделку
    • Приорететная поддержка
    • идеально подходит для студентов, у которых нет времени для решения заданий
    Готовое решение
    $0 / весь год.
    • Вы можите продать(исполнителем) или купить(заказчиком) готовое решение
    • Вам предоставят готовое решение
    • Будет предоставлено в минимальные сроки т.к. задание уже готовое
    • Вы получите базовую гарантию 8 дней
    • Вы можете заработать на материалах
    • подходит как для студентов так и для преподавателей
    Я исполнитель
    $0 / весь год.
    • Вы профессионал своего дела
    • У вас есть опыт и желание зарабатывать
    • Вы хотите помочь в решении задач или написании работ
    • Возможно примерение функции гаранта на сделку
    • подходит для опытных студентов так и для преподавателей

    Комментарии


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

    Алгоритмы и теория алгоритмов

    Термины: Алгоритмы и теория алгоритмов