Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Lektsii_TRPO / 2_Recurs_Lec_2

.doc
Скачиваний:
66
Добавлен:
12.03.2015
Размер:
1.54 Mб
Скачать
2m элементов, которые далее объединить в упорядоченные мас­сивы длиной не более 4m и так далее, пока не по­лучится один отсортированный искомый массив. Вложенный характер сортировок и слияний частей массива является основой для рекурсивной реализации алгоритма. Таким образом, чтобы получить отсортирован­ный массив этим методом, нужно многократно "сливать" два упорядоченных отрезка массива в один упорядоченный отрезок. При этом другие части массива не затрагиваются. Пример сортировки слиянием приведен ниже.

Листинг 2.7. { Рекурсивная сортировка слиянием }

Procedure AddSort ( L, R: Integer); {сортировка слиянием}

{ L, R , m – границы объединяемых подмассивов: (L..m), (m+1..R) }

Var m :Integer;

Begin

If L < R Then Begin

m := (L+R) Div 2; {определяем границы подмассивов}

AddSort ( L, m ); {сортируем первый подмассив a[L..m]}

AddSort ( m+1, R ); {сортируем второй подмассив a[m+1..R}

Merge ( L, m, R ); {объединяем слиянием два подмассива}

End;

End;

После сортировки двух массивов - частей необходимо их слияние процедурой Merge. Основная идея алгоритма Merge состоит в сравнении очередных элементов каждой части, выяснении, какой из них меньше, переносе его в динамический массив с, ко­торый является вспомогательным, и продвижении по тому массиву-части, из которого взят элемент. Когда одна из частей будет пройдена до конца, ос­танется только перенести в с оставшиеся элементы второй части, сохраняя их порядок. При завершении процедуры массив с копируется в а и память, отведенная для нее, освобождается. Динамический характер массива с обусловлен ее необходимой переменной длиной. Можно отметить, метод сортировки слиянием имеет производительность в среднем O(n log n).

Листинг 2.7.1. {Объединяет слиянием два отсортированных подмассива}

Procedure Merge ( L, m, R :Integer);

Var i, j, k, s, t :Integer;

Begin

i := L; { i – индекс для первого подмассива}

j := m+1; { j – индекс для второго подмассива}

k:=1; { k – индекс для массива с }

While ( i <= m ) And ( j <= R) Do { цикл слияния в массив с }

If a[i] < a[j] Then Begin c[k] := a[i]; Inc(i); Inc(k); End

Else Begin c[k] := a[j]; Inc(j); Inc(k); End;

If i>m Then Begin s:=j; t:=R; End

Else Begin s:=i; t:=m; End;

For i:=s To t Do

Begin c[k] := a[i]; Inc(k); End; {перенос остатка}

Move ( c, a[L], 2*(R - L+1) ); {копирование c→a}

End;

Вызов: ….. Ввод массива а; AddSort( 1, n ); Вывод массива а; ……

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

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

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

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

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

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

Соседние файлы в папке Lektsii_TRPO