Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2051 / 0 | Длительность: 63:16:00
Лекция 6:

Элементарные методы сортировки

Аннотация: Рассмотрены элементарные методы сортировки небольших файлов либо файлов со специальной структурой.

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

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

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

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

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

Правила игры

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

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

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

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

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

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

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

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

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

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

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

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

#include <iostream.h>
#include <stdlib.h>
template <class Item>
  void exch(Item &A, Item &B)
    { Item t = A; A = B; B = t; }
template <class Item>
  void compexch(Item &A, Item &B)
    { if (B < A) exch(A, B); }
template <class Item>
  void sort(Item a[], int l, int r)
    { for (int i = l+1; i <= r; i++)
        for (int j = i; j > l; j-- )
compexch(a[j-1], a[j]);
    }
int main(int argc, char *argv[])
  { int i, N = atoi(argv[1]), 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-1);
    for (i = 0; i < N; i++) cout << a[i] << " ";
    cout << endl;
  }
      

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

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

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

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

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

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

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

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

 Пример устойчивой сортировки

увеличить изображение
Рис. 6.1. Пример устойчивой сортировки

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

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

Упражнения

6.1. Детская игрушка состоит из i карт, отверстие в которых подходит к колышку в i-ой позиции, причем i принимает значения от 1 до 5. разработайте метод для помещения карт на колышки, считая, что по виду карты невозможно сказать, подходит ли она к тому или иному колышку (обязательно нужно попробовать).

6.2. Для выполнения карточного трюка нужно, чтобы колода карт была упорядочена по мастям (пики, потом червы, трефы и бубны), а внутри каждой масти — по старшинству. Попросите нескольких своих друзей выполнить эту задачу (перед каждой попыткой перетасуйте карты!) и запишите методы, которыми они пользовались.

6.3. Объясните, как вы будете сортировать колоду карт при условии, что карты необходимо укладывать в ряд лицевой стороной вниз, и допускается только проверка значений двух карт и (при необходимости) обмен этих карт местами.

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

6.5. Приведите все последовательности из трех операций сравнения-обмена для упорядочения трех элементов.

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

6.7. Напишите клиентскую программу, которая проверяет устойчивость используемой подпрограммы сортировки.

6.8. Проверка упорядоченности массива после выполнения функции sort не доказывает, что сортировка работает. Почему?

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

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

Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?

Никита Андриянов
Никита Андриянов
Илья Клементьев
Илья Клементьев
Египет, Украина
Igor Yasnytsky
Igor Yasnytsky
Украина, Kyiv