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

информатика.Методы сортировок

.pdf
Скачиваний:
10
Добавлен:
04.06.2015
Размер:
2.25 Mб
Скачать

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

продолжаютсядо тех пор, пока части массива не станут содержать только по

одному элементу, иными словами, пока исходный массив не будет разбит на

п частей, что соответствует количеству его элементов, если число п является

степенью ДВОЙКИ (n=2k) , глубина рекурсии равна k=log2.n. Например, как показано на рис.ll, если исходный массив содержит восемь элементов (8=23 ),

то глубина рекурсии равна 3. Если число n является степенью двойки, глубина рекурсии равна 1+ Iog2n (округленноезначение).

Исходный вызов

функции mergesort (уровень О) обращается к

функции merge только

один раз. Затем функция merge осуществляет

слияние п элементов, выполняя 3*n-l операций. На первом уровне рекурсии выполняются два вызова функции mergesort И, следовательно, функции merge. Каждый из этих двух вызовов приводит К слиянию n/2 элементов и требует выполнения 3*(n/2)-1 операций. Таким образом, на этом уровне выполняется 2*(3*(n/2)-1 )==3*n-2 операций. На т-м уровне рекурсии выполняются 2т вызовов функции merge. Каждый из этих вызовов приводит К слиянию n 12т элементов, а общее количество операций равно 3*(nI2n1) - 2 . В целом, 2т рекурсивных вызова функции merge порождает 3*n-2т операций. Таким образом, на каждом уровне рекурсии выполняется О(n) операций. Поскольку количество уровней рекурсии равно Iog2n или Iog2n + 1, в наихудшем и среднем вариантах функция mergesort имеет сложность

O(n*log2n). Посмотрите на рис.3 и еще раз убедитесь, что величина

O(n*log2n) растет намного быстрее, чем величинао(n2).

Хотя алгоритм сортировки слиянием имеет чрезвычайно высокое быстродействие, у него есть один недостаток. Для выполнения операции

Объединить упорядоченныеподмассивы

theArray[first mid] и

theArray[mid+l last]

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

неприемлемым.

Выстряя сортировка

Рассмотрим два первых шага решения задачи о поиске k-ro наименьшегоэлементамассива theArray[fist... last},

Выберите в массиве thеАrrаvfПrst... last[опорный элемент р Поделитемассив theArray[first... last] относительно элементар

Разбиение, показанное на рис. 12, характеризуется тем, что все элементы множества S 1 == theArray [first... pivotlndex-1} меныпе опорного элементар,

30

а множество S2=theArray [pivotlndex+ 1. . .last] состоит из элементов,

больших или равных опорному. Хотя из этого свойства не следует, что массив упорядочен, из него вытекает чрезвычайно полезный факт: если массив упорядочен, элементы, стоящие на позициях от first до pivotlndex-l,

остаются на своих местах, хотя их позиции относительно друг друга могут

измениться. Аналогичноеутверждениевыполняетсяи для элементов, стоящих

на позицияхот pivotlndex+ 1 до last.

Опорный элемент в полученном упорядоченном массиве останется на

своем месте.

Такое разбиение массива определяет рекурсивный характер алгоритма. Разбиение массива относительно опорного элемента р

порождает две задачи сортировки меньшего размерасортировка

леВОЙ(Sl) и правой/Б-) частей массива. Решив эти две задачи, мы

получим решение исходной задачи.

82

 

 

 

 

>= Р

 

 

 

fiгst

pivotlndex

last

РИСУНОК 12. Разбиение относительно опорного элемента

Иными словами, разбиение массива перед рекурсивными вызовами оставляет опорный элемент на своем месте и гарантирует, что левый и правый отрезки массива окажутся упорядоченными. Кроме того, алгоритм быстрой

сортировки конечен: размеры левого и правого отрезка массива меньше размера

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

когда массив состоит из одного элемента. Это следует из того факта, что опорный элемент р не принадлежит ни одному из массивов 8! и 82.

Псевдокод алгоритма быстрой сортировки выглядит следующим образом.

qиiсksоrt(inоиt theArray.·ltemArray,

in first: integer, in last: integer)

// Упорядочиваетмассив theArray[first.. Zast]

If (first < last)

Выбрать опорныйэлементр из массива theArray[first..Zast] Разбить массив theArray[first..Zast] относительно

опорного элементар

//Разбиение имет вид theArray[fir.st..pivotlndex. . Zast]

//Упорядочиваеммассив 81

qиiсksоrt(thеАl/8rау, first, pivot Index-1)

// Упорядочиваем массив 82

qиiсksоrt (theArray, pivotZndex+l, Zast)

31

} // Конец оператора if

// еслиfirst >= Таз», ничего не делаем

kSmaZZ(in k:integer~ in theArray:/temArray,

in first: integer~ in Zast: integer):Item Туре

//Возвращаетзначениеk-го наименьшегоэлементамассива

//theArray{first.. Zast}.

Выбрать в массиве theArray[first..last}

опорныйэлементр

Разбитьмассив theArray[first..Zast}

относительно элементар

If (k < pivotZndex -

first + 1)

 

 

return kSmaZZ(k~

theArray~

first,

pivotZndex-Z)

else if (k = = pivotZndex -

.first

+ 1)

return р

 

 

 

 

else

 

 

 

 

return kSmaZZ(k-(рivоtZndех-:fir..s,t+I},

спе.Ат-ау.

pivotlndex+l,

last)

 

Функция kSmall

вызывается

рекурсивно, только если одна из

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

kSmall(k, theArray, first, last)

 

или

 

 

 

 

 

 

 

 

kSmall(k, theArray, first,

pivotlndex-1)

 

kSmall(k-(рivоtlndex-first+1),

 

 

 

 

theArray, pivotl ndex+ 1, last)

 

 

 

 

 

 

quicksort(theArray, first, last)

quicksort(theArray, first, pivotlndex-1)

quicksort(theArray, first, pivotlndex+1,last)

РИСУНОК 13. Сравнение фУНКЦИЙ k~Sтall и qиiсkSО1~t

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

32

Функция, предназначенная для разбиения массива, получает в качестве аргумента отрезок theArray[first. .last}. Функция должна

распределить элементы массива, руководствуясь следующим

правилом: в множество Stвключаютсяэлементы, меньшие опорного, а в

множество S2 -

остальные. Как показано на рис.12, множество S 1

является отрезком массива theArray [first. .pivot

Index-l}, а

множество S2 -

отрезком массива theArray [pivotlndex+l. . last}. Как

выбрать опорный элемент? Если элементы массива записаны в произвольном порядке, в качестве опорного можно выбрать любой элемент, например theArray [first}. (Более детально процедура выбора опорного элемента будет рассмотрена позднее.) При разбиении массива опорный элемент удобно помещать в ячейку theArray [first}, независимо от того, какой именно элемент выбран в качестве

опорного.

Часть массива, в которой находятся элементы, еще не распределенные по отрезкам 81 и 82, называется неопределенноЙ. Итак, рассмотрим массив, изображенный на рис.14. Индексы first, lastS1, firstUnknown и last разделяют массив на три части. Отношения между опорным элементом и элементами неопределенной части theArray

{firstUnknown..last] неизвестны.

Опорный элемент 82 Неопределенная часть

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

>=р

 

?

р

 

 

 

 

 

 

 

 

 

 

 

first

Last81

firstUrlknown

last

РИСУНОК 14. Инвариант алголритма разбиения

в процессе разбиения массива должно выполняться следующее

условие.

Элементы множества 81 должны быть меньше опорного элемента, а элементы множества 82 - больше или равны ему.

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

опорного элемента, считался неопределенным.

1аs tS1 == fi rs t

first Unknown == first + 1

33

Исходное состояние массива изображено на рис. 15.

Неопредепенная часть

 

 

р

?

 

 

 

 

fiгst

fiгstUnknown

last

lastS1

Рисунок 15. Исходное состояние массива

На каждом шаге алгоритма разбиения проверяется один элемент из неопределенной части. В зависимости от его значения он помещается в множество 8] или 82. Таким образом, на каждом шаге размер неопределенной части уменьшается на единицу. Алгоритм останавливается, когда размер неопределенной части становится равным нулю, т.е. выполняется условие firstVnknown > last.

Рассмотрим псевдокод этого алгоритма.

partition (inоиt theArray:ltemArray,

in first: integer, in last: infegel", оиt pivotlndex: integer)

//Разделяетмассив theArray[first.. last}

//Инициализация

Выбрать опорныйэлемент и поменять его местами

с элементом theArray[first}

р = theArray[fir5.~t} // р - опорный элемент

// Задаем пустыемножества 8 }и 82., а неопределенную

//часть массива инициализируемотрезком

//theArray[fir,st+ 1 .. Газ«]

Za"f)tSZ == ./ir,st firstUnknown == first + 1

// Определяеммножества 8]и S2.

while (firstUnknown < = last)

(

//Вычисляем индекс самого левого элемента

//неопределеннойчасти массива

if (theArray[firstUnknown) < р)

Поместить элемент theArray[firstUnknown} в S]. else

Поместить элемент theArray[fir,-~;tUnknоwnJ в S2.

} // Конец оператора while

34

//Ставимопорныйэлементмеждумножествами8/ и 82.

//и запоминаемего новый индекс

Поменятьместами theArray [first} и theArray [lastSZ} pivotZndex == ZastSl

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

Поместить элемент theArray [firstUnknown} в множество 8/. Множество 8/ И неопределенная часть, как правило, не являются смежными.Обычно между ними располагается множество 82. Однако эту операцию можно выполнить более эффективно. Элемент thеАrrау[firstVnknоwn}можно поменять местами с первым элементом множества S2., т.е. с элементом theArray[lastSI+ l}, как показано на рис. 16. Как быть с элементом множества 52, который был помещен в ячейку

theArray[firstVnknown}? Если увеличить индекс firstVnknown на единицу,

этот элемент становится самым правым в множестве82. Таким образом, для переноса элемента theArray [firstUnknown} в массив 8/ необходимо

выполнить следующиешаги.

Поменятьместамиэлементы theArray[firstlnknown}

и theArray[lastSl+ 1]

Увеличить индекс ZastSZ на единицу Увеличить индекс firstUnknown на единицу

Эта стратегия остается верной, даже если множество S2 пусто. В этом случае величина last81 +1 равна индексу firstUnknown, и элемент просто остается на своем месте. Инвариантпри этом не нарушается.

Поместить элемент theArray[firstUnknown} в множество 82. Эту операцию легко выполнить. Напомним, что индекс крайнего правого элемента множества 82 pabehfirstUnknown-l, т.е. множество S2 инеизвестная часть являются смежными (рис. 17). Таким образом, чтобы переместить элемент theArray [firstUnknown} в множество 82, нужно просто увеличить индекс firstUnknown на единицу, расширяя множество 82вправо.Инвариант

при этом не нарушается.

После переноса всех элементовиз неопределеннойчасти в множестваS1 И 82 остается решить последнюю задачу. Нужно поместить опорный элемент между множествами 8/ и 82. Обратите внимание, что элемент theArray [last81} является крайним правым элементом множества S/.

35

Перестановка

 

 

81

 

 

82

 

 

Неопределенная часть

 

 

А

_

 

~ ----------- _ // ~ ------­ -------­ --------.,

 

(

 

 

<,у

 

 

 

(

\

 

 

 

 

 

 

 

 

 

р

 

 

>=р.

>=р

 

 

?

 

 

 

 

 

 

 

 

 

 

 

 

first

 

Last81 Last81 +1

firstUnknown

last

Рисунок 16. Перенос элемента thеАrlАау[firstUnknО"ИJn}в множестве Sl перестановкис элементомtheArray[lastSI+ 1) с последующим увеличением индексов lastSI иjirstUnknоwn на единицу

Опорный

элемент

 

 

82

 

 

Неопредепенная часть

 

 

 

 

 

 

 

 

 

 

 

/----------­

_._----~

 

 

 

 

 

 

 

 

 

(

 

\

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

>=р

 

 

 

?

 

 

р

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

first

Last81

firstUnknown

last

Рисунок 17. Перенос элемента thеАrrау[firstUnknо"И)n}в

множестве S2 после увеличения

индексаjirst Unknown на единицу

 

 

 

Если поменять его местами с опорным элементом, тот станет на правильное место. Следовательно,оператор

pivotlndex = ZastSI

позволяет определить индекс опорного элемента. Этот индекс можно использовать в качестве границы между множествами 8/ и 82.. Результаты трассировки алгоритма разбиения массива, состоящего из шести целых чисел, когда опорным является первый элемент, показаны на рис. 18.

36

Опорный

элемент

Исходный

массив

 

Опорный

 

 

 

 

 

 

 

 

Неопределеннаячасть

 

 

 

 

 

 

 

 

 

 

 

 

 

 

элемент

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Неопределеннаячасть

 

 

 

 

 

 

 

 

 

 

 

 

 

Неопределенная часть

 

27

 

12

38 .

 

 

Опорный

 

 

 

 

 

 

 

 

 

 

 

 

элемент

81

 

 

 

52

 

 

Неопределенная часть

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Опорный

 

 

 

 

 

 

 

 

 

 

 

 

 

элемент

 

81

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

27

 

 

12

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Опорный

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

81

 

 

 

52

 

 

 

элемент

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

27

 

 

 

16

 

39

 

 

 

 

 

12

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

)~

 

~

 

 

 

 

 

 

 

 

 

 

 

 

 

::I:

Ф

 

 

 

 

 

 

 

 

 

 

 

 

Q..:!

 

 

 

 

 

 

 

 

 

 

 

 

о

ф

 

 

 

 

 

 

 

 

 

 

 

 

с:

r:::;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Полное

 

 

 

 

81

 

0(')

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

16

 

12

 

27

 

39

 

 

 

 

 

 

 

 

 

 

 

 

 

разбиение

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

fiгstUnknown =1 (указывает на 38) 38 принадлежит множеству 82

Множество 81 пусто;

12 принадлежит множеству 81 J поэтому меняем местами 38 и 12

39 принадлежит множеству 82

27 принадлежит множеству 52

16 принадлежит множеству 81, поэтому меняем местами 38 и 16

Множества51 и 82 определены

Помещаем опорный элемент между множествами81 и 82

РИСУНОК 18. Первое разбиение массива, когда опорным является первый элемент

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

Все элементы множества S/. (theArray[first+ 1..last S/}) меньше опорного, а все элементымножестваS2 (theArray [lastSI.. /irstUnknown-1J) больше или равны опорному

Напомним, что для определения правильности алгоритма с помощью его инвариантов,необходимовыполнитьчетыре шага.

37

1. Инвариант должен быть истинным с самого начала, до выполнения цикла. В алгоритме разбиения опорным элементом является theArray [first},неизвестной частью­ отрезок массива theArray [first+l.. last}, а множества 8/ и 82. пусты. Очевидно, что при этих условиях инвариант является

истинным.

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

сохраняться и после переноса.

3. Инвариант должен определять корректность алгоритма. Иными словами, из истинности инварианта должна следовать корректность алгоритма. Выполнение алгоритма разбиения прекраrцается, когда неопределенная область становится пустой. В этом случае каждый элемент отрезка theArray[first+ 1. .last] должен принадлежать либо множеству 81,либо множеству 82. В любом случае из корректности инварианта следует, что алгоритм достиг своей

цели.

4. ЦИКЛ должен быть конечным. Иными словами, нужно

показать, что выполнение цикла завершится после конечного

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

завершается.

Рассмотрим функцию на языке С++, реализующую алгоритм quick8ort.B ней для выбора опорного элемента используется функция choosePivot, а функция swap работает, как и раньше, в алгоритме selection8ort. Для упор ния массива theArray, состоящего из n элементов, выполняется вызов

quickSort(theArray, О, n-l).

void cho()~\'ePivot(DataTypetheArray[}, int..fir5;t, int ILМ·t)~·

;1;1 -------------------------------------------------------------------­

//Выбираетопорныйэлементдля алгоритмабыстройсортировки.

//Меняет его местамис первым элементоммассива.

//Предусловие:отрезокtheArray[first..last} - массив;

//first < == last.

//Постусловие:элементtheArra})[first} является опорным.

//----------------------------------------------------------------------------­

//Реализацияэтой функциипредоставляетсячитателям.

38

voidpartition(DataТуре theArray[},

intjirst, int last, int& pivotlndex)

hI ----------------------------------------------------------------------------­

//Разбиваетмассивдля быстройсортировки.

//Предусловие:отрезокtheArray[first..last} -)\1ассив;

//jirst < == last.

//Постусловие:массивtheArray[first..last} разбит

//следующимобразом:

//S1 == theArray[{irst..pivotlndex-l] < pivot

//theArray [pivotlndex] ==== pivot

// 82 == theArray {pivotlndex +1.. last] > = pivot

// Вызываемыефункции: choosePivot и swap. /

!/ ----------------------------------------------------------------------------­

{

// Помещаемопорныйэлементв ячейкуtheArray[first}

choosePivot(theArray, jirst, last);

Гииа'Гуре pivot == theArray[first}; // Копируем

// опорныйэлемент

//В исходномположениивсе элементы, кроме опорного,

//принадлежатнеопределеннойчастимассива

int lastSl == jirst; // Индекспоследнегоэлемента //Jwножесmваk-')l

intjirstlJnknown ==jirst + 1,.

// Индекс первого элемента

 

// неопределенной части

//Переносимэлементыодин за другим.

//пока неопределеннаячастьмассиване станетпустой

/о, (:.firstUnknown < == last; + +jirstUnknown)

{

//Инвариант:theArray[first+ 1..last81] < pivot

//theArray[lastSl + 1...firstUnknОИJn-l} > = pivot

//Переносимэлемент из неопределеннойчасти

//в множество~S1 или S2

if(theArray[firstUnknown] < pi1t'of)

f l

// Элементпринадлежитмножеству81

++lastSI;

swap(theArray[firstUnknown}, theArray[lastSZ]) ;

~} // Конецоператора if

//Иначеэлемент принадлежитмножеству82 } //Конецоператора.fOr

//Поставить опорныйэлемент на соответствующееместо

// изапомнить его индекс

swap (theArray[first), theArray[la\,t8l]);

pivotZndex = ZastS1,.

39