Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции по МОИ (глава2).doc
Скачиваний:
8
Добавлен:
05.11.2018
Размер:
397.82 Кб
Скачать

Алгоритм 11. Быстрсорт

Вход. Массив элементов Array[i], 1in.

Выход. Элементы массива Array, расположенные в порядке возрастания.

Метод. В основе алгоритма лежит рекурсивная процедура, заключающаяся в разбиении всего множества элементов массива на три группы. В первую группу S1 входят элементы, меньшие некоторого элемента Array[j]. Во вторую группу S2 – элементы, равные ему. А в третью группу S3 входят элементы, большие выбранного. Далее алгоритм Быстрсорт рекурсивно применяется к элементам первой S1 и третьей S3 групп, и т.д. В результате получается отсортированный по возрастанию массив элементов. Результат сортировки хранится в том же самом массиве Array в следующей последовательности

Быстрсорт(S1), S2, Быстрсорт(S3)

Теорема 7. Алгоритм 11 упорядочивает последовательность из n элементов за среднее время O(nlogn).

Доказательство. Корректность алгоритма 11 доказывается прямой индукцией по длине последовательности n. Докажите это самостоятельно. Чтобы проще было анализировать время работы, допустим, что все элементы в последовательности различны. Это допущение максимизирует размеры последовательностей S1 и S3 и тем самым максимизирует среднее время, затрачиваемое в рекурсивных вызовах процедуры Быстрсорт. Пусть T(n) – среднее время, затрачиваемое алгоритмом Быстрсорт на упорядочение последовательности из n элементов. Ясно, что T(0) = T(1) = b для некоторой постоянной b.

Допустим, что разбивающий элемент a=Array[j] является i-м наименьшим элементом среди n элементов последовательности. Тогда на два рекурсивных вызова Быстрсорт тратится среднее время T(i+1) и T(n–i+1) соответственно. Т.к. i принимает равновероятные значения между 0 и n–1, а итоговое построение последовательности Быстрсорт(S1 + S2 + S3) очевидно занимает время cn для некоторой постоянной c, то

T(n)  cn + для n  2

Алгебраические преобразования в последнем выражении приводят к неравенству

T(n)  cn + (2.2)

Покажем, что при n2 справедливо неравенство T(n)  knlnn, где k= 2c + 2b и b = T(0) = T(1). Для базиса (n=2) неравенство T(n)  2c + 2b непосредственно вытекает из (2.2). Для проведения шага индукции запишем (2.2) в виде

T(n)  cn ++ (2.3)

Т.к. функция ilni вогнута, легко показать, что

(2.4)

Подставляя (2.4) в (2.3), получаем

T(n)  cn ++

Поскольку n2 и k= 2c + 2b, то cn + . Таким образом, неравенство T(n)  knlnn следует из последнего выражения. 

Рассмотрим две детали, важные с точки зрения практической реализации алгоритма. Первая – способ выбора «произвольного» элемента Array[j], по которому осуществляется разбиение всего массива на три группы в процедуре Быстрсорт. При реализации этого шага может возникнуть искушение встать на простой путь и выбирать, скажем, всегда первый элемент всего сортируемого массива. Подобный выбор мог бы послужить причиной ухудшения временной сложности работы всего алгоритма. Последовательность сортируемых элементов часто бывает уже «как-то» отсортирована, поэтому первый элемент мал с вероятностью выше средней. Предположим, что последовательность уже отсортирована. Тогда на последующем шаге (при указанном выборе разделяющего элемента) последовательность будет сокращаться всего на один элемент. Получится заведомо квадратичная скорость работы. Лучшей техникой для выбора разбивающего элемента было бы использование генератора случайных чисел для порождения целого числа j, 1jn, и выбора Array[j] в качестве разбивающего. Более простой способ (но не более быстрый в среднем) – взять выборку элементов из массива и найти её медиану в качестве разбивающего элемента. Например, можно взять первый и последний элементы массива и найти их медиану.

Вторая деталь – как эффективно разбить сортируемый массив на три упомянутые группы. Так как процедура Быстрсорт вызывает себя рекурсивно, её аргумент (массив Array) всегда будет находиться в последовательных элементах массива. Поэтому выгодно располагать группы, состоящие из сортируемых элементов на том же самом месте. Кроме того, обычно бывает эффективнее применять рекурсивный алгоритм Быстрсорт к группам S1 и S2+S3 вместо поиска места в массиве Array, где оканчивается группа S2 и начинается S3 для применения Быстрсорт только к элементам S3. Это происходит потому, что, как правило, количество элементов в S2 гораздо меньше, чем в S3.

По-видимому, самый лёгкий способ разбить элементы массива на том же месте – использовать два указателя на крайние элементы сортируемого массива, назовём их in_min и in_max. Вначале целая переменная indi равна in_min, а целая переменная indj равна in_max. После выбора разбивающего элемента Array[j] indi оказывается слева, а indj – справа от него. Далее эти индексы начинают двигаться навстречу друг другу: indi двигается вправо до тех пор, пока не встретит элемент массива, больший Array[j], а indj двигается влево – пока не встретит элемент, меньший Array[j]. После чего элементы массива Array[indi] и Array[indj] меняются местами, и движение индексов навстречу продолжается. Элемент программы, реализующий это разбиение, показан ниже.

Процедура разбиения массива на два подмассива

// Разбиение массива array на два подмассива, первый содержит элементы,

// меньшие dummy, а второй – бóльшие

indi=in_min; indj=in_max;

while(indi<=indj)

{

while ((array[indj]>=dummy)&&(indj>=in_min)) indj--;

while ((array[indi]<dummy)&&(indi<=in_max)) indi++;

if (indi<indj)

{

// Поменять местами элементы array[indi] и array[indj]

temp=array[indi]; array[indi++]=array[indj]; array[indj--]=temp;

}

if ((indj<in_min)||(indj<0)) // Если найден наименьший элемент dummy

{

indj=in_min; indi++;

temp=array[in_min]; array[in_min]=dummy; array[in_min+separator-1]=temp;

}

}

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

Пример 2.5. Рассмотрим на примере, как осуществляется данное разбиение массива. Пусть задан массив Array, показанный на рис. 11, а.

Номер i

0

1

2

3

4

5

6

7

8

9

Array[i]

6

9

3

1

2

4

7

1

8

5

Рис. 11,а

Разобьём данный массив по элементу Array[5]=4. While-оператор в строке  уменьшает indj с 9 до 7, поскольку числа Array[9]=5 и Array[8]=8 не меньше Array[5], но Array[7]=1<4. While-оператор в строке  не увеличивает значения indi=0, поскольку Array[0]=6>4. Поэтому в строке  переставляются местами Array[0] и Array[7]. Далее полагается indi=1, indj=6, и получается массив рис. 11,б

Номер i

0

1

2

3

4

5

6

7

8

9

Array[i]

1

9

3

1

2

4

7

6

8

5

indi  indj

Рис. 11,б

Результат после следующего срабатывания цикла показан на рис. 11,в.

Номер i

0

1

2

3

4

5

6

7

8

9

Array[i]

1

2

3

1

9

4

7

6

8

5

indi  indj

Рис. 11,в

Следующий цикл даёт состояние массива, показанное на рис. 11, г.

Номер i

0

1

2

3

4

5

6

7

8

9

Array[i]

1

2

3

1

9

4

7

6

8

5

indj indi

Рис. 11,г

Поскольку в последней ситуации indj < indi, то разбиение массива на два подмассива закончено. 

2.5. Порядковые статистики

С упорядочением тесно связана задача нахождения k-го наименьшего элемента в n-элементной последовательности.

Определение. k-м наименьшим элементом в последовательности a1, a2,an называется такой элемент b этой последовательности, что ai<b не более чем для k–1 значений i и aib не менее чем для k значений i.

Одно из очевидных решений состоит в следующем: упорядочить эту последовательность в порядке неубывания её элементов и взять k-й элемент. Как уже было сказано, это потребует порядка nlogn операций сравнения. Аккуратно применяя стратегию «разделяй и властвуй», можно найти k-й наименьший элемент за O(n) шагов. Важный частный случай: k=n/2; в этом случае середина последовательности отыскивается за линейное время.