на тему рефераты
 
Главная | Карта сайта
на тему рефераты
РАЗДЕЛЫ

на тему рефераты
ПАРТНЕРЫ

на тему рефераты
АЛФАВИТ
... А Б В Г Д Е Ж З И К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Э Ю Я

на тему рефераты
ПОИСК
Введите фамилию автора:


Реферат: VB, MS Access, VC++, Delphi, Builder C++ принципы(технология), алгоритмы программирования


T(N)    = 1 + 4 * T(N - 1)

        = 1 + 4 *(1 + 4 * T(N - 2))

        = 1 + 4 + 16 * T(N - 2)

        = 1 + 4 + 16 * (1 + 4 * T(N - 3))

        = 1 + 4 + 16 + 64 * T(N - 3)

        = ...

        = 40 + 41 + 42 + 43 + ... + 4K * T(N - K)

Раскрыв это уравнение до тех пор, пока не будет выполнено условие остановки рекурсии T(1)=1, получим:

T(N) = 40 + 41 + 42 + 43 + ... + 4N-1

Это уравнение можно упростить, воспользовавшись соотношением:

X0 + X1 + X2 + X3 + ... + XM = (XM+1 - 1) / (X - 1)

После преобразования, уравнение приводится к виду:

T(N)    = (4(N-1)+1 - 1) / (4 - 1)

        = (4N - 1) / 3

=====90

С точностью до постоянных, эта процедура выполняется за время порядка O(4N). В табл. 5.5 приведены несколько первых значений функции времени выполнения. Если вы внимательно посмотрите на эти числа, то увидите, что они соответствуют рекурсивному определению.

Этот алгоритм является типичным примером рекурсивного алгоритма, который выполняется за время порядка O(CN), где C — некоторая постоянная. При каждом вызове подпрограммы Hilbert, она увеличивает размерность задачи в 4 раза. В общем случае, если при каждом выполнении некоторого числа шагов алгоритма размер задачи увеличивается не менее, чем в C раз, то время выполнения алгоритма будет порядка O(CN).

Это поведение противоположно поведению алгоритма поиска наибольшего общего делителя. Процедура GCD уменьшает размерность задачи в 2 раза при каждом втором своем вызове, и поэтому время ее выполнения порядка O(log(N)). Процедура построения кривых Гильберта увеличивает размер задачи в 4 раза при каждом своем вызове, поэтому время ее выполнения порядка O(4N).

Функция (4N-1)/3 — это экспоненциальная функция, которая растет очень быстро. Фактически, она растет настолько быстро, что вы можете предположить, что это не слишком эффективный алгоритм. В действительности работа этого алгоритма занимает много времени, но есть две причины, по которым это не так уж и плохо.

Во-первых, ни один алгоритм для построения кривых Гильберта не может быть намного быстрее. Кривые Гильберта содержат множество отрезков линий, и любой рисующий их алгоритм будет требовать достаточно много времени. При каждом вызове процедуры Hilbert, она рисует три линии. Пусть L(N) — суммарное число линий, из которых состоит кривая Гильберта порядка N. Тогда L(N) = 3 * T(N) = 4N - 1, поэтому L(N) также порядка O(4N). Любой алгоритм, рисующий кривые Гильберта, должен вывести O(4N) линий, выполнив при этом O(4N) шагов. Существуют другие алгоритмы построения кривых Гильберта, но они занимают почти столько же времени, сколько и этот алгоритм.

@Таблица 5.5. Число рекурсивных вызовов подпрограммы Hilbert

=====91

Второй факт, который показывает, что этот алгоритм не так уж плох, заключается в том, что кривые Гильберта 9 порядка содержат так много линий, что экран большинства компьютерных мониторов при этом оказывается полностью закрашенным. Это неудивительно, так как эта кривая содержит 262.143 отрезков линий. Это означает, что вам вероятно никогда не понадобится выводить на экран кривые Гильберта 9 или более высоких порядков. На каком‑то порядке вы столкнетесь с ограничениями языка Visual Basic и вашего компьютера, но, скорее всего, вы еще раньше будете ограничены максимальным разрешением экрана.

Программа Hilbert, показанная на рис. 5.4, использует этот рекурсивный алгоритм для рисования кривых Гильберта. При выполнении программы не задавайте слишком большую глубину рекурсии (больше 6) до тех пор, пока вы не определите, насколько быстро выполняется эта программа на вашем компьютере.

Рекурсивное построение кривых Серпинского

Как и кривые Гильберта, кривые Серпинского (Sierpinski curves) — это самоподобные кривые, которые обычно определяются рекурсивно. На рис. 5.5 показаны кривые Серпинского 1, 2 и 3 порядка.

Алгоритм построения кривых Гильберта использует всего одну подпрограмму для рисования кривых. Кривые Серпинского проще рисовать, используя четыре отдельных процедуры, которые работают совместно. Эти процедуры называются SierpA, SierpB, SierpC и SierpD. Это процедуры с косвенной рекурсией — каждая процедура вызывает другие, которые затем вызывают первоначальную процедуру. Они рисуют верхнюю, левую, нижнюю и правую части кривой Серпинского, соответственно.

На рис. 5.6 показано, как эти процедуры работают совместно, образуя кривую Серпинского 1 порядка. Подкривые изображены стрелками, чтобы показать направление, в котором они рисуются. Отрезки, соединяющие четыре подкривые, нарисованы пунктирными линиями.

@Рис. 5.4. Программа Hilbert

=====92

@Рис. 5.5. Кривые Серпинского

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

Например, для разбиения кривой типа A, первый диагональный отрезок разбивается на кривую типа A, за которой следует кривая типа B. Затем рисуется без изменений горизонтальный отрезок из исходной кривой типа A. Наконец, второй диагональный отрезок разбивается на кривую типа D, за которой следует кривая типа A. На рис. 5.7 показано, как кривая типа A второго порядка образуется из нескольких кривых 1 порядка. Подкривые изображены жирными линиями.

На рис. 5.8 показано, как полная кривая Серпинского 2 порядка образуется из 4 подкривых 1 порядка. Каждая из подкривых обведена контурной линией.

Можно использовать стрелки ä и ã для обозначения типа линий, соединяющих подкривые (тонкие линии на рис. 5.8), тогда можно будет изобразить рекурсивные отношения между четырьмя типами кривых так, как это показано на рис. 5.9.

@Рис. 5.6. Части кривой Серпинского

=====93

@Рис. 5.7. Разбиение кривой типа A

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

Private Sub SierpA(Depth As Integer, Dist As Single)

    If Depth = 1 Then

        Line -Step(-Dist, Dist)

        Line -Step(-Dist, 0)

        Line -Step(-Dist, -Dist)

    Else

        SierpA Depth - 1, Dist

        Line -Step(-Dist, Dist)

        SierpB Depth - 1, Dist

        Line -Step(-Dist, 0)

        SierpD Depth - 1, Dist

        Line -Step(-Dist, -Dist)

        SierpA Depth - 1, Dist

    End If

End Sub

@Рис. 5.8. Кривые Серпинского, образованные из меньших кривых Серпинского

=====94

@Рис. 5.9. Рекурсивные соотношения между кривыми Серпинского

Кроме процедур, которые рисуют каждую из основных кривых, потребуется еще процедура, которая по очереди вызывает их все для создания законченной кривой Серпинского.

Sub Sierpinski (Depth As Integer, Dist As Single)

    SierpB Depth, Dist

    Line -Step(Dist, Dist)

    SierpC Depth, Dist

    Line -Step(Dist, -Dist)

    SierpD Depth, Dist

    Line -Step(-Dist, -Dist)

    SierpA Depth, Dist

    Line -Step(-Dist, Dist)

End Sub

Анализ времени выполнения программы

Чтобы проанализировать время выполнения этого алгоритма, необходимо определить число вызовов для каждой из четырех процедур рисования кривых. Пусть T(N) — число вызовов любой из четырех основных подпрограмм основной процедуры Sierpinski при построении кривой порядка N.

Если порядок кривой равен 1, кривая каждого типа рисуется только один раз. Прибавив сюда основную процедуру, получим T(1) = 5.

При каждом рекурсивном вызове, процедура вызывает саму себя или другие процедуры четыре раза. Так как эти процедуры практически одинаковые, то T(N) будет одинаковым, независимо от того, какая процедура вызывается первой. Это обусловлено тем, что кривые Серпинского симметричны и содержат одно и то же число кривых разных типов. Рекурсивные уравнения для T(N) выглядят так:

T(1) = 5

T(N) = 1 + 4 * T(N-1)             для N > 1.

Эти уравнения почти совпадают с уравнениями, которые использовались для оценки времени выполнения алгоритма, рисующего кривые Гильберта. Единственное отличие состоит в том, что для кривых Гильберта T(1) = 1. Сравнение значений этих уравнений показывает, что TSierpinski(N) = THilbert(N+1). В конце предыдущего раздела было показано, что THilbert(N) = (4N - 1) / 3, поэтому TSierpinski(N) = (4N+1 - 1) / 3, что также составляет O(4N).

=====95

Так же, как и алгоритм построения кривых Гильберта, этот алгоритм выполняется за время порядка O(4N), но это не так уж и плохо. Кривая Серпинского состоит из O(4N) линий, поэтому ни один алгоритм не может нарисовать кривую Серпинского быстрее, чем за время порядка O(4N).

Кривые Серпинского также полностью заполняют экран большинства компьютеров при порядке кривой, большем или равном 9. При каком‑то порядке, большем 9, вы столкнетесь с ограничениями языка Visual Basic и возможностей вашего компьютера, но, скорее всего, вы еще раньше будете ограничены предельным разрешением экрана.

Программа Sierp, показанная на рис. 5.10, использует этот рекурсивный алгоритм для рисования кривых Серпинского. При выполнении программы, задавайте вначале небольшую глубину рекурсии (меньше 6), до тех пор, пока вы не определите, насколько быстро выполняется эта программа на вашем компьютере.

Опасности рекурсии

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

Бесконечная рекурсия

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

@Рис. 5.10 Программа Sierp

=====96

Private Function BadFactorial(num As Integer) As Integer

    BadFactorial = num * BadFactorial (num - 1)

End Function

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

Private Function BadFactorial2(num As Double) As Double

    If num = 0 Then

        BadFactorial2 = 1

    Else

        BadFactorial2 = num * BadFactorial2(num-1)

    End If

End Function

Следующая версия функции Fibonacci является более сложным примером. В ней условие остановки рекурсии прекращает выполнение только нескольких путей рекурсии, и возникают те же проблемы, что и при выполнении функции BadFactorial2, если входные значения отрицательные или не целые.

Private Function BadFib(num As Double) As Double

    If num = 0 Then

        BadFib = 0

    Else

        BadFib = BadPib(num - 1) + BadFib (num - 2)

    End If

End Function

И последняя проблема, связанная с бесконечной рекурсией, заключается в том, что «бесконечная» на самом деле означает «до тех пор, пока не будет исчерпано стековое пространство». Даже корректно написанные рекурсивные процедуры будут иногда приводить к переполнению стека и аварийному завершению работы. Следующая функция, которая вычисляет сумму N + (N - 1) + … + 2 +1, приводит к исчерпанию стекового пространства при больших значениях N. Наибольшее возможное значение N, при котором программа еще будет работать, зависит от конфигурации вашего компьютера.

Private Function BigAdd(N As Double) As Double

    If N <= 1 Then

        BigAdd = 1

    Else

        BigAdd = N + BigAdd(N - 1)

    End If

End Function

=====97

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

Потери памяти

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

Существует несколько способов уменьшения этих накладных расходов. Во‑первых, не следует использовать большого количества ненужных переменных. Даже если подпрограмма не использует их, Visual Basic все равно будет отводить память под эти переменные. Следующая версия функции BigAdd еще быстрее приводит к переполнению стека, чем предыдущая.

Private Function BigAdd(N As Double) As Double

Dim I1 As Integer

Dim I2 As Integer

Dim I3 As Integer

Dim I4 As Integer

Dim I5 As Integer

    If N <= 1 Then

        BigAdd = 1

    Else

        BigAdd = N + BigAdd (N - 1)

    End If

End Function

Если вы не уверены, нужна ли переменная, используйте оператор Option Explicit и закомментируйте определение переменной. При попытке выполнить программу, Visual Basic сообщит об ошибке, если переменная используется в программе.

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

Лучшим решением будет определение переменных в процедуре при помощи зарезервированного слова Static. Статические переменные используются совместно всеми экземплярами процедуры, и системе не нужно отводить память под новые копии переменных при каждом вызове подпрограммы.

Необоснованное применение рекурсии

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

=====98

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

С другой стороны, применение рекурсии ухудшает алгоритм вычисления чисел Фибоначчи. Для вычисления Fib(N), алгоритм вначале вычисляет Fib(N - 1) и Fib(N - 2). Но для вычисления Fib(N - 1) он должен сначала вычислить Fib(N - 2) и Fib(N - 3). При этом Fib(N - 2) вычисляется дважды.

Предыдущий анализ этого алгоритма показал, что Fib(1) и Fib(0) вычисляются Fib(N + 1) раз во время вычисления Fib(N). Так как Fib(30) = 832.040 то, чтобы вычислить Fib(29), приходится вычислять одни и те же значения Fib(0) и Fib(1) 832.040 раз. Алгоритм вычисления чисел Фибоначчи тратит огромное количество времени на вычисление этих промежуточных значений снова и снова.

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

Похожая проблема существует и в функции факториала. Для входного значения N глубина рекурсии для факториала и функции BigAdd равна N. Функция факториала не может быть вычислена для таких больших входных значений, которые допустимы для функции BigAdd. Максимальное значение факториала, которое может уместиться в переменной типа double, равно 170! » 7,257E+306, поэтому это наибольшее значение, которое может вычислить эта функция. Хотя эта функция приводит к глубокой рекурсии, она вызывает переполнение до того, как наступит переполнение стека.

Когда нужно использовать рекурсию

Эти рассуждения могут заставить вас думать, что рекурсия всегда нежелательна. Но это определенно не так. Многие алгоритмы являются рекурсивными по своей природе. И хотя любой алгоритм можно переписать так, чтобы он не содержал рекурсии, многие алгоритмы сложнее понимать, анализировать, отлаживать и поддерживать, если они написаны нерекурсивно.

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

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

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

======99

Хвостовая рекурсия

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

Private Function Factorial(num As Integer) As Integer

    If num <= 0 Then

        Factorial = 1

    Else

        Factorial = num * Factorial(num - 1)

    End If

End Function

Private Function GCD(A As Integer, B As Integer) As Integer

    If B Mod A = 0 Then

        GCD = A

    Else

        GCD = GCD(B Mod A, A)

    End If

End Function

Private Function BigAdd(N As Double) As Double

    If N <= 1 Then

        BigAdd = 1

    Else

        BigAdd = N + BigAdd(N - 1)

    End If

End Function

Во всех этих функциях, последнее действие перед завершением функции — это рекурсивный шаг. Этот тип рекурсии в конце процедуры называется хвостовой рекурсией (tail recursion или end recursion).

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

Рассмотрим общий случай рекурсивной процедуры:

Private Sub Recurse(A As Integer)

    ' Выполняются какие‑либо действия, вычисляется B, и т.д.

    Recurse B

End Sub

======100

Эту процедуру можно переписать без рекурсии как:

Private Sub NoRecurse(A As Integer)

    Do While (not done)

        ' Выполняются какие‑либо действия, вычисляется B, и т.д.

        A = B

    Loop

End Sub

Эта процедура называется устранением хвостовой рекурсии (tail recursion removal или end recursion removal). Этот прием не изменяет время выполнения программы. Рекурсивные шаги просто заменяются проходами в цикле While.

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

Некоторые компиляторы автоматически устраняют хвостовую рекурсию, но компилятор Visual Basic этого не делает. В противном случае, функция BigAdd, приведенная в предыдущем разделе, не приводила бы к переполнению стека.

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

Private Function Factorial(ByVal N As Integer) As Double

Dim value As Double

    value = 1#         ' Это будет значением функции.

    Do While N > 1

        value = value * N

        N = N - 1      ' Подготовить аргументы для "рекурсии".

    Loop

    Factorial = value

End Function

Private Function GCD(ByVal A As Double, ByVal B As Double) As Double

Dim B_Mod_A As Double

    B_Mod_A = B Mod A

    Do While B_Mod_A <> 0

        ' Подготовить аргументы для "рекурсии".

        B = A

        A = B_Mod_A

        B_Mod_A = B Mod A

    Loop

    GCD = A

End Function

Private Function BigAdd(ByVal N As Double) As Double

Dim value As Double

    value = 1#     ' ' Это будет значением функции.

    Do While N > 1

        value = value + N

        N = N - 1  ' подготовить параметры для "рекурсии".

    Loop

    BigAdd = value

End Function

=====101

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

Для функции BigAdd, тем не менее, разница огромна. Рекурсивная версия приводит к переполнению стека даже для довольно небольших входных значений. Поскольку нерекурсивная версия не использует стек, она может вычислять результат для значений N вплоть до 10154. После этого наступит переполнение для данных типа double. Конечно, выполнение 10154 шагов алгоритма займет очень много времени, поэтому возможно вы не станете проверять этот факт сами. Заметим также, что значение этой функции совпадает со значением более просто вычисляемой функции N * N(N + 1) / 2.

Программы Facto2, GCD2 и BigAdd2 демонстрируют эти нерекурсивные алгоритмы.

Нерекурсивное вычисление чисел Фибоначчи

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

Страницы: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33


на тему рефераты
НОВОСТИ на тему рефераты
на тему рефераты
ВХОД на тему рефераты
Логин:
Пароль:
регистрация
забыли пароль?

на тему рефераты    
на тему рефераты
ТЕГИ на тему рефераты

Рефераты бесплатно, реферат бесплатно, курсовые работы, реферат, доклады, рефераты, рефераты скачать, рефераты на тему, сочинения, курсовые, дипломы, научные работы и многое другое.


Copyright © 2012 г.
При использовании материалов - ссылка на сайт обязательна.