- •Министерство образования Российской Федерации
- •Содержание
- •1.2 Скорость роста функций
- •1.3 Анализ алгоритмов; время работы в лучшем, худшем случаях и в среднем
- •1.4 Типы данных, структуры данных и абстрактные типы данных
- •1.5 Динамические множества
- •2 Алгоритмы сортировок
- •2.1 Понятие внутренней и внешней сортировки
- •2.2 Сортировка вставками
- •2.3 Сортировка слиянием
- •2.3.1 Описание алгоритма
- •2.3.2 Анализ времени работы алгоритмов «разделяй и властвуй»
- •2.3.2 Анализ времени работы сортировки слиянием через рекуррентное соотношение
- •2.3.3 Анализ времени работы сортировки слиянием через геометрическую интерпретацию
- •2.4 Пирамидальная сортировка
- •2.4.1 Введение в алгоритм
- •2.4.2 Сохранение основного свойства кучи
- •2.4.3 Построение кучи
- •2.5 Быстрая сортировка
- •2.5.1 Введение в алгоритм
- •2.5.2 Описание
- •2.5.3 Разбиение массива
- •2.5.4 Особенности работы быстрой сортировки
- •2.6 Особенности реализации алгоритмов сортировки; сортировка за линейное время
- •2.6.1 Введение
- •2.6.2 Разрешающее дерево сортировки сравнениями
- •2.7 Цифровая сортировка
- •2.8 Сортировка вычерпыванием
- •2.8.1 Описание алгоритма
- •2.8.2 Вероятностный анализ времени работы сортировки вычерпыванием
- •2.8.3 Анализ времени работы сортировки вычерпыванием через геометрическую интерпретацию
- •2.9 Сортировка подсчетом
- •2.9.1 Описание алгоритма
- •2.9.2 Анализ времени работы
- •3 Элементарные и нелинейные структуры данных
- •3.1 Элементарные структуры: список, стек, очередь, дек
- •3.1.1 Список Линейный однонаправленный список
- •Линейный двунаправленный список
- •Двунаправленный список с фиктивными элементами
- •Циклические списки
- •Циклический однонаправленный список
- •Циклический двунаправленный список
- •3.1.2 Стек
- •3.1.3 Очередь
- •3.1.3 Дек
- •3.2 Нелинейные структуры данных
- •3.2.1 Представление корневых деревьев в эвм
- •Обходы деревьев
- •3.2.2 Двоичные деревья Спецификация двоичных деревьев
- •Реализация
- •Обходы двоичных деревьев
- •3.2.3 Двоичные деревья поиска Основные операции
- •Минимум и максимум
- •Следующий и предыдущий элементы
- •Добавление и удаление
- •Случайные деревья поиска
- •Оптимальные деревья поиска
- •4 Хеширование
- •4.1 Введение
- •4.2 Прямая адресация; таблицы с прямой адресацией
- •4.3 Хеш – таблицы; возникновение коллизий и их разрешение
- •Разрешение коллизий с помощью цепочек
- •Анализ хеширования с цепочками
- •4.4 Способы построения хеш – функций Выбор хорошей хеш-функции
- •Ключи как натуральные числа
- •Деление с остатком
- •Умножение
- •Универсальное хеширование
- •4.5 Открытая адресация; способы вычисления последовательности испробованных мест: линейная последовательность проб, квадратичная последовательность проб, двойное хеширование
- •Линейная последовательность проб
- •1 / (1 – )
- •5 Основные принципы разработки алгоритмов
- •5.1 Введение в теорию графов
- •5.1.1 Графы
- •5.1.2 Представление графов
- •5.2 Алгоритмы на графах: поиск в ширину, поиск в глубину
- •5.2.1 Поиск в ширину (волновой алгоритм)
- •5.2.2 Анализ поиска в ширину
- •5.2.3 Деревья поиска в ширину
- •5.2.4 Поиск в глубину
- •5.2.5 Анализ поиска в глубину
- •5.2.6 Свойства поиска в глубину
- •5.2.7 Классификация рёбер
- •5.3 Топологическая сортировка, задача о разбиении графа на сильно связанные компоненты
- •5.3.1 Топологическая сортировка
- •5.3.2 Сильно связные компоненты
- •5.4 Алгоритм построения минимального остовного дерева
- •5.4.1 Остовные деревья минимальной стоимости
- •5.4.2 Построение минимального покрывающего дерева
- •5.4.3 Алгоритмы Крускала и Пpимa
- •5.4.4 Алгоритм Крускала
- •5.4.5 Алгоритм Прима
- •5.5 Задача нахождения кратчайших путей на графах; алгоритм Флойда; алгоритм Дейкстры
- •5.5.1 Нахождение кратчайшего пути
- •5.5.2 Алгоритм Дейкстры
- •5.5.3 Алгоритм Флойда
- •5.6 Поиск с возвратом
- •5.6.1 Введение
- •5.6.2 Переборные алгоритмы
- •5.6.3 Метод ветвей и границ
- •5.6.4 Метод альфа-бета отсечения
- •5.6.5 Локальные и глобальные оптимальные решения
- •5.7 Метод декомпозиции ( «Разделяй и властвуй»)
- •5.7.1 «Ханойские башни»
- •5.8 Жадные алгоритмы и динамическое программирование
- •5.8.1 Задача о выборе заявок
- •5.8.2 Дискретная задача о рюкзаке
- •5.8.3 Непрерывная задача о рюкзаке
- •5.8.4 Числа Фибоначчи
- •5.8.5 Задача триангуляции многоугольника
- •5.8.6 Дп, жадный алгоритм или что-то другое?
2.5 Быстрая сортировка
2.5.1 Введение в алгоритм
Эту сортировку называют быстрой, потому что на практике она оказывается самым быстрым алгоритмом сортировки из тех, что оперируют сравнениями (т.е. из класса обменных сортировок). Время его работы для массива из п чисел в худшем случае составляет (n2), на практике этот алгоритм является одним из самых быстрых: математическое ожидание времени работы составляет O(nlog n), причём множитель при nlog n довольно мал. Кроме того, быстрая сортировка не требует дополнительной памяти и сохраняет эффективность для систем с виртуальной памятью.
Этот алгоритм является ярким примером реализации принципа «разделяй и властвуй». Как показывают теоретические выкладки, наиболее эффективным в общем случае оказывается разделение задачи на две равные по сложности части, что здесь и делается.
На каждом шаге алгоритма сначала выбирается «средний» элемент, затем переставляются элементы массива так, что массив разделился на две части. Первая часть содержит элементы, меньше «среднего» и, возможно, равные ему. Вторая часть содержит элементы больше «среднего» и, возможно, равные ему. После такого деления массива остается только отсортировать его части по отдельности, с которыми поступаем аналогично (делим на две части). И так до тех пор, пока эти части не окажутся состоящими из одного элемента, а массив из одного элемента всегда отсортирован. В случае, когда массив содержит только одинаковые элементы, выбор «среднего» элемента не производится и сортировка не осуществляется.
2.5.2 Описание
Быстрая сортировка (quicksort), как и сортировка слиянием, основана на принципе «разделяй и властвуй». Сортировка участка A[p..r] происходит следующим образом.
• Элементы массива А переставляются так, чтобы любой из элементов А[p],...,A[q] был не больше любого из элементов A[q + 1],..., А[r], где q – некоторое число в интервале р ≤ q < r. Эта операция называется разделением (partition).
• Процедура сортировки рекурсивно вызывается для массивов A[р..q] и A[q + 1..r].
После этого массив A[р..r] отсортирован.
Листинг 2.6 – Процедура быстрой сортировки
2.5.3 Разбиение массива
Основной шаг алгоритма – процедура Partition, которая переставляет элементы массива А[р..r] нужным образом:
Листинг 2.7 – Процедура разбиения массива
Работа процедуры Partition показана на рис. 2.5. Элемент х = А[p] выбирается в качестве «граничного»; массив A[p..q] будет содержать элементы, не большие х, а массив A[q + 1..r] – элементы, не меньшие х. Идея состоит в том, чтобы накапливать элементы, не большие х, в начальном отрезке массива (А[р..i]), а элементы, не меньшие х – в конце (A[j..r]). В начале оба «накопителя» пусты: i = р – 1, j = r + 1.
Внутри цикла while (в строках 5-8) к начальному и конечному участкам присоединяются элементы (как минимум по одному). После выполнения этих строк А[i] ≥ х ≥ A[j]. Если поменять А[i] и A[j] местами, то их можно будет присоединить к начальному и конечному участкам.
В момент выхода из цикла выполнено неравенство i ≥ j. При этом массив разбит на части А[р],...,A[j] и A[j + 1],..., А[r]; любой элемент первой части не превосходит любого элемента второй части. Процедура возвращает значение j.
Хотя идея процедуры очень проста, сам алгоритм содержит ряд тонких моментов. Например, не очевидно, что индексы i и j не выходят за границы промежутка от р до r в процессе работы. Другой пример: важно, что в качестве граничного значения выбирается А[р], а не, скажем, А[r]. В последнем случае может оказаться, что А[r] – самый большой элемент массива, и в конце выполнения процедуры будет i = j = r, так что возвращать q = j будет нельзя – нарушится требование q < r, и процедура Quicksort зациклится.
Время работы процедуры Partition составляет (n), где п = r – р + 1.
Рисунок 2.5 – Работа процедуры Partition
Незакрашенные элементы относятся к уже сформированным фрагментам, закрашенные ещё не распределены. (а) Начальное состояние массива, начальный и конечный куски пусты. Элемент х = А[р] = 5 используется в качестве граничного. (б) Результат первого прохода цикла while (строки 4-8). (в) Элементы А[i] и A[j] меняются местами (строка 10). (г) Результат второго прохода цикла while. (д) Результат третьего (последнего) прохода цикла while. Поскольку i ≥ j, процедура останавливается и возвращает значение q = j. Элементы слева от A[j] (включая сам этот элемент) не больше, чем х = 5, а элементы справа от A[j] не меньше, чем х = 5.
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}
Листинг 2.8 – Функция нахождения разделяющего элемента быстрой сортировки
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}
Листинг 2.9 – Быстрая сортировка