Автор: Пользователь скрыл имя, 27 Октября 2013 в 23:14, курс лекций
Алгоритм - это точное предписание, определяющее вычислительный процесс, ведущий от варьируемых начальных данных к искомому результату.
ЭВМ в настоящее время приходится не только считывать и выполнять определенные алгоритмы, но и хранить значительные объемы информации, к которой нужно быстро обращаться. Эта информация в некотором смысле представляет собой абстракцию того или иного фрагмента реального мира и состоит из определенного множества данных, относящихся к какой-либо проблеме.
j := j-1;
массива внутренний цикл не будет выполняться ни разу, поэтому метод имеет временную сложность Tmin(n), пропорциональную O(n). Можно заметить, что метод использует любой частичный порядок, и чем в большей степени массив исходно упорядочен, тем быстрее он закончит работу. В отличие от предыдущего метода, этот не требует дополнительной памяти, но сохраняет порядок элементов с одинаковыми значениями.
2.5.2.3. Сортировка методом Шелла
Метод Шелла является усовершенствованием метода простого включения, который основан на том, что включение использует любой частичный порядок. Но недостатком простого включения является то, что во внутреннем цикле элемент A[i] фактически сдвигается на одну позицию. И так до тех пор, пока он не достигнет своего места в отсортированной части. (На самом деле передвигалось место, оставленное под A[i]). Метод Шелла позволяет преодолеть это ограничение следующим интересным способом (рис. 42).
Вместо включения A[i] в подмассив предшествующих ему элементов, его включают в подсписок, содержащий элементы A[i - h], A[i - 2h], A[i - 3h] и так далее, где h - положительная константа. Таким образом, формируется массив, в котором «h-серии» элементов, отстоящие друг от друга на h, сортируются отдельно.
Конечно, этого недостаточно: процесс возобновляется с новым значением h, меньшим предыдущего. И так до тех пор, пока не будет достигнуто значение h = 1.
В настоящее время неизвестна последовательность hp hi_1, hi-2, hj, оптимальность которой доказана. Для достаточно больших массивов рекомендуемой считается такая последовательность, что hi+i = 3h+1, а hi = 1. Начинается процесс с hm, что hm > [n/9]. Иногда значение h вычисляют проще: hm = h/2, h1 = 1, hm = n/2. Это упрощенное вычисление h и будем использовать далее.
Теперь запишем алгоритм:
procedure ShellSort(n: integer;
var A: array[1..n] of integer); {Процедура сортировки Шелла} var
h, i, j, Tmp: integer; begin
{Вычисляем величину h}
h := n div 2;
{Собственно сортировка} while h > 0 do begin
for i := 1 to n-h do begin
j := i;
while j > 0 do begin
{Сравниваем элементы, отстоящие друг от друга} {на расстояние, кратное h} if A[j] > A[j+h] then begin
{Меняем элементы}
Tmp := A[j+h];
A[j+h] := A[j];
A[j] := Tmp;
j := j - h;
end else begin
{Досрочно завершаем цикл с параметром j} j := 0; end; end; end;
{Уменьшаем размер серии} h := h div 2; end; end;
Как показывают теоретические выкладки, которые здесь приводить не будем, сортировке методом Шелла требуется в среднем 1,66n1'25 перемещений. Порядок элементов влияет на количество итераций внутреннего цикла while. Дополнительной памяти данный алгоритм не требует, но и не гарантирует сохранение порядка элементов с одинаковыми значениями.
2.5.2.4. Сортировка простым извлечением.
В этом методе массив также делится на уже отсортированную часть A[i+1], A[i+1], A[n] и еще не отсортированную A[1], A[2], A[i]. Но здесь из неотсортированной части на каждом шаге извлекается максимальный элемент, просматривая ее заново на каждом шаге. Этот элемент будет минимальным элементом отсортированной части, так как все большие его элементы были извлечены на предыдущих шагах, поэтому ставим извлеченный элемент в начало отсортированной части, точнее меняем его с A[i] местами (рис. 43).
Рис. 43. Сортировка простым извлечением
Теперь запишем алгоритм.
procedure ExtractSort(n: integer;
var A: array[1..n] of integer); {Процедура сортировки простым извлечением} var
i, j, MaxIndex, Tmp: integer; begin
for i := n downto 2 do begin
{Ищем максимальный элемент в неотсортированной части} MaxIndex := 1; for j := 2 to i do
if A[j] > A[MaxIndex] then MaxIndex := j; {Меняем найденный элемент с первым из отсортированных}
Tmp := A[i]; A[i] := A[MaxIndex]; A[MaxIndex] := Tmp; end; end;
Простое извлечение во всех случаях имеет временную сложность, пропорциональную O(n2) (два вложенных цикла, зависящих от n линейно и не зависящих от порядка элементов). Также следует отметить, что данный алгоритм не требует дополнительной памяти и не гарантирует сохранение порядка элементов с одинаковыми значениями.
2.5.2.5. Древесная сортировка
При использовании этой сортировки в массиве постоянно поддерживается такой порядок, при котором максимальный элемент всегда будет оказываться в A[1]. Сортировка называется древесной, потому что в этом методе используется структура данных, называемая двоичным деревом. При чем используется представление дерева в виде массива (см. п. 1.3.4.4) и при сортировке используется тот же способ расположения вершин дерева в массиве.
Пусть A[1]...A[n] - массив, подлежащий сортировке. Вершинами дерева будут числа от 1 до n; о числе A[i] будем говорить как о числе, стоящем в вершине i. В процессе сортировки количество вершин дерева будет сокращаться. Число вершин текущего дерева будем хранить в переменной k. Таким образом, в процессе работы алгоритма массив A[1]...A[n] делится на две части: в A[1]...A[k] хранятся числа на дереве, а в A[k+1]...A[n] хранится уже отсортированная в порядке возрастания часть массива - элементы, уже занявшие свое законное место (рис. 44).
На каждом шаге алгоритм будет изымать максимальный элемент дерева, и помещать его в отсортированную часть, на освободившееся в результате сокращения дерева место.
Договоримся о терминологии. Вершинами дерева считаются числа от 1 до текущего значения переменной k. У каждой вершины s могут быть потомки 2s и 2s + 1. Если оба этих числа больше k, то потомков нет; такая вершина называется листом. Если 2s = k, то вершина s имеет ровно одного потомка (2s).
Для каждого s из 1...k рассмотрим «поддерево» с корнем в s: оно содержит вершину s и всех ее потомков. Вершина s называется регулярной , если стоящее в ней число - максимальный элемент s-подде-рева; s-поддерево называется регулярным, если все его вершины регу-
лярны. В частности, любой лист образует регулярное одноэлементное поддерево.
Теперь запишем алгоритм сортировки:
procedure TreeSort (n: integer;
var A: array[1..n] of integer); {Процедура древесной (пирамидальной) сортировки} var
u, k: integer; procedure Exchange(i, j: integer); {Процедура обмена двух элементов} var
Tmp: integer; begin
Tmp := A[i]; A[i] := A[j];
A[j] := Tmp;
end; {Exchange}
procedure Restore(s: integer);
{Процедура восстановления регулярности поддерева с корнем s} var
t: integer; begin
t:=s; {начинаем с корня поддерева}
while ((2*t+1 <= k) and (A[2*t+1] > A[t])) or
((2*t <= k) and (A[2*t] > A[t])) do begin {Пока не просмотрено все поддерево и вершина t нерегулярна} if (2*t+1 <= k) and (A[2*t+1] >= A[2*t]) then begin {Меняем корень поддерева с его правым потомком} Exchange(t, 2*t+1);
t := 2*t+1; {переход к правому потомку} end else begin
{Меняем корень поддерева с его левым потомком} Exchange(t, 2*t);
t := 2*t; {переход к правому потомку} end; end;
end; {Restore} begin
k:= n;
u:= n;
while u <> 0 do begin 138
Restore(u); u := u - 1; end;
while k <> 1 do begin
Exchange(1, k);
k := k - 1;
Restore(1); end;
end; {TreeSort}
В качестве вспомогательных процедур используются процедуры обмена двух элементов Exchange и процедура восстановления регулярности s-поддерева в корне - Restore. Первая процедура введена исключительно для лучшей наглядности. Вторая требуется для восстановления регулярности поддерева, которая может нарушиться после обменов элементов.
Рассмотрим восстановление регулярности подробнее. Пусть в s-под-дереве все вершины, кроме разве что вершины t, регулярны. Рассмотрим потомков вершины t. Они регулярны, и потому содержат наибольшие числа в своих поддеревьях. Таким образом, на роль наибольшего числа в t-поддереве могут претендовать число в самой вершине t и числа, содержащиеся в ее потомках (в первом случае вершина t регулярна, и все в порядке).
После обмена вершина t становится регулярной (в нее попадает максимальное число t-поддерева). Не принявший участия в обмене потомок остается регулярным, а принявший участие может и не быть регулярным. В остальных вершинах s-поддерева не изменились ни числа, ни поддеревья их потомков (разве что два элемента поддерева поменялись местами), так что регулярность не нарушилась.
Эта же процедура может использоваться для того, чтобы сделать 1-поддерево регулярным на начальной стадии сортировки (см. первый цикл в теле основной процедуры).
Преимущество этого алгоритма перед другими в том, что он, имея максимальную временную сложность Tmax(n), пропорциональную O(nlog n) (внутри внешнего цикла зависящего от n линейно вызывается процедура Restore, требующая порядка log n действий), не требует дополнительной памяти порядка O(n).
2.5.2.6. Сортировка методом пузырька
Сортировка методом пузырька - один из наиболее широко известных алгоритмов сортировки. В этом методе массив также делится на две части: отсортированную и неотсортированную. На каждом шаге метода осуществляется просмотр от меньших индексов к большим по неотсортированной части, каждый раз сравнивая два соседних элемента. Если они не упорядочены между собой (меньший следует за большим), то меняем их местами. Тем самым за один проход путем последовательных обменов наибольший элемент неотсортированной части сдвинется к ее концу (рис. 45).
Алгоритм называют пузырьковой сортировкой, потому что на каждом шаге наибольший элемент неотсортированной части подобно пузырьку газа в воде всплывает к концу массива.
Заметим, что в том случае, когда за очередной проход не было сделано ни одного обмена, массив уже отсортирован, и следующие проходы можно пропустить. Для отслеживания такой ситуации введем логическую переменную Flag - признак совершения обмена на очередном проходе.
Теперь запишем алгоритм:
procedure BubleSort(n: integer;
var A: array[1..n] of integer); {Процедура сортировки методом пузырька} var
i, j, Tmp: integer; Flag: boolean; begin
for i := n-1 downto 1 do begin Flag := false; for j := 1 to i do
if A[j] > A[j+1] then begin
Tmp := A[j]; A[j] := A[j+1]; A[j+1] := Tmp; Flag := true; end;
if not Flag then Break; end; end;
Этот алгоритм имеет среднюю и максимальную временные сложности, пропорциональную O(n2) (два вложенных цикла, зависящих от n линейно) и не требует дополнительной памяти. Введение переменной Flag и прерывание работы в случае отсортированного массива позволяет свести минимальную временную сложность к O(n). Также следует отметить, что данный алгоритм не требует дополнительной памяти и сохраняет порядок элементов с одинаковыми значениями.
2.5.2.7. Быстрая сортировка (Хоара)
Эту сортировку называют быстрой, потому что на практике она оказывается самым быстрым алгоритмом сортировки из тех, что оперируют сравнениями.
Этот алгоритм является ярким примером реализации принципа «разделяй и властвуй». Как показывают теоретические выкладки, наиболее эффективным в общем случае оказывается разделение задачи на две равные по сложности части, что здесь и делается.
На каждом шаге алгоритма сначала выбирается «средний» элемент, затем переставляются элементы массива так, что массив разделился на две части. Первая часть содержит элементы меньше «среднего» и, возможно, равные ему. Вторая часть содержит элементы больше «среднего» и, возможно, равные ему. После такого деления массива остается только отсортировать его части по отдельности, с которыми поступаем аналогично (делим на две части). И так до тех пор, пока эти части не окажутся состоящими из одного элемента, а массив из одного элемента всегда отсортирован (рис. 46). В случае, когда массив содержит только одинаковые элементы, выбор «среднего» элемента не производится и сортировка не осуществляется.
Разделение массива на две части производится следующим образом. Устанавливаем один курсор на левую границу массива, а второй - на правую границу. Затем осуществляем перемещение курсоров навстречу друг другу до тех пор, пока они не пересекутся. При перемещении курсоров сравниваем значения текущих элементов со «средним». Находим левый текущий элемент, больший «среднего», и правый текущий элемент, меньший «среднего» (т. е. элементы, которые находятся «не на своем месте»). Осуществляем обмен этих элементов.
Выбор «среднего» - задача непростая, так как требуется, не производя сортировку, найти элемент со значением максимально близким к среднему. Здесь, конечно, можно просто выбрать произвольный элемент (обычно выбирают элемент, стоящий в середине сортируемого подмассива), но пойдем чуть дальше: из трех элементов (самого левого, самого правого и стоящего посередине) выберем средний:
procedure HoarSort(n: integer;
var A: array[1..n] of integer); {Процедура сортировки Хоара}
function FindMedium(L, R: integer): integer; {Нахождение индекса "среднего" элемента} var
MedIndex, {индекс "среднего" элемента}
Left, Right, Median: integer; begin
Left := A[L]; Right := A[R]; Median := A[(L+R) div 2];
{Берем два крайних элемента и один из середины массива} if (Left = Median) and (Median = Right) then begin
{Если все три элемента одинаковы, то ищем неравный им}
i := L;
while (A[i] = Median) and (i < R) do i := i + 1; {Если найден неравный элемент, то берем его третьим} if A[i] <> Median then Median := A[i]; end;
if (Left = Median) and (Median = Right) then begin
{Все элементы массива одинаковы и "средний" не найден}
FindMedium := 0; end else begin
{Выбираем "средний" из трех разных элементов}
if Left <= Median then if Median <= Right then
MedIndex := (L+R) div 2
else
if Left <= Right then MedIndex := R else MedIndex := L
else
if Left >= Right then MedIndex := (L+R) div 2
else
if Left >= Right then
MedIndex := R else MedIndex := L; FindMedium := MedIndex; end;
end; {FindMedium}
procedure QuickSort(L, R: integer);
var
MedItem, {значение "среднего" элемента}
MedIndex, {индекс "среднего" элемента}
Tmp, i, j: integer; {вспомогательные переменные} begin
MedIndex := FindMedium(L, R); if MedIndex <> 0 then begin
{Сортируем, если найден "средний" элемент}
MedItem := A[MedIndex];
{Разбиваем массив на две части}
i := L; j := R;
while i <= j do begin
{Ищем первый слева элемент, больший, чем MedItem} while A[i] < MedItem do i := i + 1; {Ищем первый справа элемент, меньший, чем MedItem} while A[j] > MedItem do j := j - 1;
if i <= j then begin {Меняем местами найденные элементы} Tmp := A[i]; A[i] := A[j];
A[j] := Tmp; i := i + 1; j := j - 1;
end; end;
{Сортируем две части массива по отдельности} if L < j then QuickSort(L, j); if i < R then QuickSort(i, R); end;
end; {QuickSort}
begin {HoarSort}
QuickSort(1,
n);
end; {HoarSort}
Заметим, что предложенный способ нахождения «среднего» элемента подмассива в худшем случае приведет к тому, что после деления, например, правая часть поделенного массива будет содержать один элемент, а левая - все остальные. В этом случае получается порядка n рекурсивных вызовов. Это значит, что необходимо будет завести дополнительную память размером, пропорциональным n, и пространственная сложность Vmax(n) будет пропорциональна O(n). В среднем и лучшем случае, можно говорить о пространственной сложности, пропорциональной O(log n).