Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Элементарные алгоритмы сортировки.doc
Скачиваний:
2
Добавлен:
10.07.2019
Размер:
193.02 Кб
Скачать

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

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

Начнем с рассмотрения простой программы тестирования методов сортировки, которая обеспечивает контекст, позволяющий выработать соглашения, необходимые для того, чтобы впоследствии им следовать. Мы также проанализируем базовые свойства различных методов сортировки; их очень важно знать при оценке возможности использования того или иного алгоритма в условиях конкретных приложений. Затем мы подробно рассмотрим реализацию трех элементарных методов; сортировки выбором, сортировки вставками и пузырьковой сортировки. После этого будут подробно анализироваться рабочие характеристики упомянутых алгоритмов. Далее мы рассмотрим сортировку методом Шелла, которой не очень-то подходит характеристика "элементарная", однако она достаточно просто реализуется и имеет много общего с сортировкой методом вставки. Углубляясь в математические свойства сортировки методом Шелла, мы займемся изучением проблемы разработки интерфейсов типов данных, наряду с программами, которые рассматривались в главах 3 и 4 и которые предназначены для распространения изучаемых алгоритмов на сортировку различных видов файлов данных, какие встречаются на практике. Затем мы рассмотрим методы сортировки по косвенным ссылкам на данные, а также методы сортировки связных списков. Данная глава завершается обсуждением специализированного метода, который целесообразно использовать, когда известно, что ключи принимают значения в ограниченном диапазоне.

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

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

6.1. Вводные замечания

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

Если сортируемый файл полностью помещается в оперативной памяти, то используемый в этом случае метод сортировки называется внутренним. Сортировка файлов, хранящихся на магнитной ленте или диске, называется внешней. Основное различие между этими двумя методами заключается в том, что в условиях внутренней сортировки доступ к любому элементу не представляет трудностей, в то время как в условиях внешней сортировки возможен только последовательный метод доступа или, по меньшей мере, доступ к блокам больших размеров. Некоторые из внешних методов сортировки рассматриваются в главе, однако большая часть исследуемых алгоритмов принадлежит к категории внутренней сортировки.

Мы будем рассматривать как массивы, так и связные списки. Как проблема сортировки массивов, так и проблема сортировки связных списков представляют несомненный интерес: в процессе разработки собственных алгоритмов мы столкнемся с некоторыми базовыми задачами, для которых лучше всего подойдет последовательное распределение элементов, для других же задач больше подходит структура связных списков. Некоторые из классических методов обладают столь высокой степенью абстракции, что могут одинаково эффективно применяться как к массивам, так и к связным спискам; в то же время другие максимально эффективно проявляют себя на каком-то одном виде указанных выше объектов сортировки. Другие виды ограничений доступа также иногда представляют определенный интерес.

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

Программа 6.1. Пример сортировки массива с помощью управляющей программы

Данная программа служит иллюстрацией принятых нами соглашений, касающихся реализации базовых алгоритмов сортировки массивов. Функция main – суть драйвер, который инициализирует массив целыми значениями (случайными значениями либо значениями, полученными из стандартного ввода), вызывает функцию sort с целью выполнения сортировки заполненного массива, после чего выводит на печать упорядоченный результат сортировки.

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

Можно внести соответствующие изменения в программу-драйвер, чтобы она смогла выполнять сортировку любых типов данных, для которых определена операция operator <, не внося при этом никаких изменений в функцию sort (см. раздел 6.7).

#include <iostream.h>

#include <stdlib.h>

template <class Item>

void exch(Item &A, Item &B)

{Item t = A; A = В; В = t;}

template <class Item>

void compexch (Item &A, Item &B)

{if (B < A) exch(A, B);}

template <class Item>

void sort (Itern a[] , int m, int r)

{ for (int i = m+1; i <= r; i++)

for (int j = i; j > m; j--)

compexch(a[j-1], a[j]);

}

int main(int argc, char *argv[])

{

int i, N = atoi(argv[l]), sw = atoi (argv[2]);

int *a = new int[N];

if (sw)

for (i = 0; i < N; i++)

a[i] = 1000*(1.0*rand()/RAND_MAX);

else

{ N = 0; while (cin >> a[N]) N++; }

sort (a, 0, N-l) ;•

for (i = 0; i < N; i++) cout << a[i] << =;

cout << endl;

}

Как уже должно быть известно из глав 3 и 4, существуют многочисленные механизмы, обеспечивающие условия для выполнения сортировки и других типов данных. Мы подробно обсудим возможности использования таких механизмов в разделе 6.7. Функция sort из программы 6.1 представляет собой приведенную к шаблону реализацию, ссылающуюся на сортируемые элементы только посредством своего первого аргумента и нескольких простейших операций над данными. Как обычно, подобный подход позволяет использовать одни и те же программные коды для Сортировки элементов разных типов. Например, если в программный код функции main программы 6.1, выполняющий генерацию, хранение и печать случайных ключей внести изменения, обеспечивающие возможность обработки чисел с плавающей точкой вместо целых чисел, то вообще отпадет необходимость какой-либо модификации функции sort. С целью достижения упомянутой гибкости (и в то же время явной идентификации переменных, в которых хранятся элементы сортировки), реализации должны быть снабжены такими параметрами, благодаря которым стала бы возможной работа с типом данных Item. На данном этапе под типом данных Item подразумевается int или float; в разделе 6.7 подробно рассматривается реализация типов данных, которые позволят выполнять сортировку элементов произвольной природы с ключами в виде чисел с плавающей точкой, строк и других типов, используя механизмы, описанные в главах 3 и 4.

Функцию sort можно заменить любой программой сортировки массивов, представленной в настоящей главе или в главах 7–10. Каждая из них предполагает необходимость сортировки элементов типа Item, каждая из них использует три аргумента: массив, левую и правую границы подмассива, подлежащего сортировке. В них также применяется операция operator <, осуществляющая сравнение ключей элементов, а также функции exch или compexch, выполняющие обмен элементов местами. Чтобы иметь возможность различать методы сортировки, будем присваивать различным программам сортировки разные имена. Переименование какой-либо из них, замена программы-драйвера или использование указателей на функции для переключения с одного алгоритма на другой в клиентской программе, подобной программе 6.1, без внесения изменений в программную реализацию алгоритма сортировки, не сопряжено ни с какими трудностями.

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

Пример функции сортировки, представленный программой 6.1, является одним из вариантов сортировки вставками (insertion sort), который более подробно исследуется в разделе 6.3. Так как в нем используются только операции сравнения и обмена, то его можно считать примером неадаптивной (nonadaptive) сортировки: последовательность операций, которые она выполняет, не зависит от порядка следования данных. И наоборот, адаптивная (adaptive) сортировка выполняет различные последовательности операций в зависимости от результата сравнения (вызов операции operator <). Неадаптивные методы сортировки интересны тем, что они достаточно просто реализуются аппаратными средствами (см. главу 11), однако большинство универсальных алгоритмов сортировки, которые будут предметом наших обсуждений, суть адаптивные методы сортировки.

Как обычно, основной характеристикой алгоритма сортировки, вызывающей наибольший интерес, является время, затрачиваемое на его выполнение. Для выполнения сортировки N элементов методом выбора, методом вставок и пузырьковым методом, которые будут рассматриваться в разделах 6.2–6.4, требуется время, пропорциональное N2, как показано в разделе 6.5. Более совершенные методы, которые исследуются в главах 7–10, могут выполнить сортировку N элементов за время, пропорциональное NlogN, однако эти методы не всегда столь же эффективны, как рассматриваемые здесь методы применительно к небольшим значениям N, а также в некоторых особых случаях. В разделе 6.6 обсуждается более совершенный метод (сортировка методом Шелла), который можно выполнить за время, пропорциональное N3/2 или даже за меньшее время, а в разделе 6.10 приводится специализированный метод (сортировка по ключевым индексам), которая для некоторых типов ключей выполняется за время, пропорциональное N.

Аналитические результаты, изложенные в предыдущем параграфе, получены на основе подсчета базовых операций (сравнение и обмен значениями), которые выполняет алгоритм. Как уже отмечалось в разделе 2.2, следует также учитывать затраты ресурсов на выполнение этих операций. Тем не менее, в общем случае мы считаем, что основное внимание целесообразно сосредоточить на изучении наиболее часто используемых операций (внутренний цикл алгоритма). Цель заключается в том, чтобы разработать эффективные и недорогие реализации эффективных алгоритмов. Имея перед собой эту цель, мы не только должны избегать неоправданных расширений внутренних циклов алгоритмов, но и искать пути удаления всевозможных команд из внутренних циклов, где это только возможно. В общем, наилучший способ уменьшения стоимости приложения – это переключение на более эффективный алгоритм; другой способ предусматривает минимизацию внутренних циклов по числу команд. Оба эти способа подробно исследуются на примере алгоритмов сортировки.

Дополнительный объем оперативной памяти, используемый алгоритмом сортировки, является вторым по важности фактором, который также будет рассматриваться. По существу, все эти методы можно разбить на три категории: те, которые выполняют сортировку на месте и не нуждаются в дополнительной памяти, за исключением, возможно, небольшого стека или таблицы; те, которые используют представление в виде связного списка или каким-то другим способом получает доступ к данным, используя для этой цели указатели или индексы массивов, в связи с чем необходима дополнительная память для размещения N указателей или индексов, и те, которые требуют дополнительной памяти для размещения еще одной копии массива, подвергаемого сортировке.

Д

Adams

1

РИСУНОК 6.1. ПРИМЕР УСТОЙЧИВОЙ СОРТИРОВКИ

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

Black

2

Brown

4

Jackson

2

Jones

4

Smith

1

Thompson

4

Washington

2

White

3

Wilson

3

Adams

1

Smith

1

Washington

2

Jackson

2

Black

2

White

3

Wilson,

3

Thompson

4

Brown

4

Jones

4

Adams

1

Smith

1

Black

2

Jackson

2

Washington

2

White

3

Wilson

3

Brown

4

Jones

4

Thompson

4

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

Определение 6.1: Говорят, что метод сортировки устойчив, если он сохраняет относительный порядок размещения элементов в файле, который содержит дублированные ключи.

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

Некоторые (но отнюдь не все) простые методы сортировки, которые рассматриваются в данной главе, суть устойчивые методы. С другой стороны, многие из сложных алгоритмов сортировки (опять-таки, не все), которые исследуются в нескольких последующих главах, таковыми не являются. В тех случаях, когда устойчивость должна быть неотъемлемым свойством, мы вводим его, добавляя перед сортировкой к каждому ключу небольшой индекс, либо расширяем ключ сортировки каким-нибудь другим способом. Выполнение такой дополнительной работы равносильно использованию обоих ключей для сортировки, представленной на рис.6.1; использование устойчивого алгоритма сортировки гораздо предпочтительней. Легко согласиться с необходимостью наличия свойства устойчивости; фактически, ряд более сложных алгоритмов, которые будут рассматриваться в следующих главах, достигают устойчивости без существенных дополнительных затрат памяти и времени.

Как уже отмечалось ранее, программы сортировки осуществляют доступ к элементам в соответствие с одним из двух следующих способов: либо доступ к ключам с последующим выполнением операции сравнения, либо {[доступ к элементам в целом с целью их перемещения. Если сортируемые элементы имеют большие размеры, не имеет смысла перемещать их в памяти, целесообразнее выполнять непрямую (indirect) сортировку: переупорядочиваются не сами элементы, а, скорее, массив указателей (индексов) так, что первый указатель указывает рта наименьший элемент, следующий – на наименьший из оставшихся и т.д. Ключи можно хранить вместе с самими элементами (если ключи большие) либо с указателями (если ключи небольших размеров). Можно переупорядочить элементы после сортировки, но часто в этом нет необходимости, поскольку теперь уже имеется возможность обращения к ним в порядке, установленном сортировкой (косвенным путем). Непрямые методы сортировки рассматриваются в разделе 6.8.