Быстрая сортировка Хоара
Продолжая тему рекурсии, познакомимся с реализацией на C# еще одного известного рекурсивного алгоритма, применяемого при сортировке массивов. Описанный ранее рекурсивный алгоритм сортировки слиянием имеет один существенный недостаток - для слияния двух упорядоченных массивов за линейное время необходима дополнительная память. Разработанный Ч. Хоаром метод сортировки, получивший название быстрого метода сортировки - QuickSort, не требует дополнительной памяти. Хотя этот метод и не является самым быстрым во всех случаях, но на практике он обеспечивает хорошие результаты. Нужно отметить, что именно этот метод сортировки встроен в класс System.Array.
Идея алгоритма быстрой сортировки состоит в том, чтобы выбрать в исходном массиве некоторый элемент M, затем в начальной части массива собрать все элементы, меньшие M. Так появляются две подзадачи размерности - k и n-k, к которым рекурсивно применяется алгоритм. Если в качестве элемента M выбирать медиану сортируемой части массива, то обе подзадачи имели бы одинаковый размер и алгоритм быстрой сортировки был бы оптимальным по времени работы. Но расчет медианы требует своих затрат времени и усложняет алгоритм. Поэтому обычно элемент M выбирается случайным образом. В этом случае быстрая сортировка оптимальна лишь в среднем, а для плохих вариантов (когда в качестве M всякий раз выбирается минимальный элемент) имеет порядок n2.
Несмотря на простоту идеи, алгоритм сложен в своей реализации, поскольку весь построен на циклах и операторах выбора. Я проводил построение алгоритма параллельно с обоснованием его корректности, введя инварианты соответствующих циклов. Текст обоснования встроен в текст метода. Приведу его, а затем дам некоторые объяснения. Вначале, как обычно, приведу нерекурсивную процедуру, вызывающую рекурсивный метод:
/// <summary>
/// Вызывает рекурсивную процедуру QSort,
/// передавая ей границы сортируемого массива.
/// Сортируемый массив tower1 задается
/// соответствующим полем класса.
public void QuickSort()
{
QSort(0,size-1);
}
Вот чистый текст рекурсивной процедуры быстрой сортировки Хоара:
void QSort(int start, int finish)
{
if(start != finish)
{
int ind = rnd.Next(start,finish);
int item = tower1[ind];
int ind1 = start, ind2 = finish;
int temp;
while (ind1 <=ind2)
{
while((ind1 <=ind2)&& (tower1[ind1] < item)) ind1++;
while ((ind1 <=ind2)&&(tower1[ind2] >= item)) ind2--;
if (ind1 < ind2)
{
temp = tower1[ind1]; tower1[ind1] = tower1[ind2];
tower1[ind2] = temp; ind1++; ind2--;
}
}
if (ind1 == start)
{
temp = tower1[start]; tower1[start] = item;
tower1[ind] = temp;
QSort(start+1,finish);
}
else
{
QSort(start,ind1-1);
QSort(ind2+1, finish);
}
}
}// QuickSort
Проведите эксперимент - закройте книгу и попробуйте написать эту процедуру самостоятельно. Если вам удастся сделать это без ошибок и она пройдет у вас с первого раза, то вы - блестящий программист и вам нужно читать другие книги. Я полагаю, что в таких процедурах ошибки неизбежны и для их исправления требуется серьезная отладка. Полагаю также, что помимо обычного тестирования полезно применять обоснование корректности, основанное на предусловиях и постусловиях, инвариантах цикла. Проектируя эту процедуру, я параллельно встраивал обоснование ее корректности. Это не строгое доказательство, но, дополняя тестирование, оно достаточно, чтобы автор поверил в корректность процедуры и представил ее на суд зрителей, как это сделал я.
/// <summary>
/// Небольшая по размеру процедура содержит три
/// вложенных цикла while, два оператора if и рекурсивные
/// вызовы. Для таких процедур задание инвариантов и
/// обоснование корректности облегчает отладку.
/// </summary>
/// <param name="start">начальный индекс сортируемой части
/// массива tower</param>
/// <param name="finish">конечный индекс сортируемой части
/// массива tower</param>
/// Предусловие: (start <= finish)
/// Постусловие: массив tower отсортирован по возрастанию
void QSort(int start, int finish)
{
if(start != finish)
//если (start = finish), то процедура ничего не делает,
//но постусловие выполняется, поскольку массив из одного
//элемента отсортирован по определению. Докажем истинность
//постусловия для массива с числом элементов >1.
{
int ind = rnd.Next(start,finish);
int item = tower1[ind];
int ind1 = start, ind2 = finish;
int temp;
/// Введем три непересекающихся множества:
/// S1: {tower1(i), start <= i =< ind1-1}
/// S2: {tower1(i), ind1 <= i =< ind2}
/// S1: {tower1(i), ind2+1 <= i =< finish}
/// Введем следующие логические условия,
/// играющие роль инвариантов циклов нашей программы:
/// P1: объединение S1, S2, S3 = tower1
/// P2: (S1(i) < item) Для всех элементов S1
/// P3: (S3(i) >= item) Для всех элементов S3
/// P4: item - случайно выбранный элемент tower1
/// Нетрудно видеть, что все условия становятся
/// истинными после завершения инициализатора цикла.
/// Для пустых множеств S1 и S3 условия P2 и P3
/// считаются истинными по определению.
/// Inv = P1 & P2 & P3 & P4
while (ind1 <=ind2)
{
while((ind1 <=ind2)&& (tower1[ind1] < item)) ind1++;
//(Inv == true) & ~B1 (B1 - условие цикла while)
while ((ind1 <=ind2)&&(tower1[ind2] >= item)) ind2--;
//(Inv == true) & ~B2 (B2 - условие цикла while)
if (ind1 < ind2)
//Из Inv & ~B1 & ~B2 & B3 следует истинность:
//((tower1[ind1] >= item)&&(tower1[ind2]<item))==true.
//Это условие гарантирует, что последующий обмен
//элементов обеспечит выполнение инварианта Inv
{
temp = tower1[ind1]; tower1[ind1] = tower1[ind2];
tower1[ind2] = temp;
ind1++; ind2--;
}
//(Inv ==true)
}
//из условия окончания цикла следует: (S2 - пустое множество)
if (ind1 == start)
//В этой точке S1 и S2 - это пустые множества, -> //(S3 = tower1)
// Нетрудно доказать, что отсюда следует истинность:
//(item = min)
// Как следствие, можно минимальный элемент сделать первым,
// а к оставшемуся множеству применить рекурсивный вызов.
{
temp = tower1[start]; tower1[start] = item;
tower1[ind] = temp;
QSort(start+1,finish);
}
else
// Здесь оба множества S1 и S3 не пусты.
// К ним применим рекурсивный вызов.
{
QSort(start,ind1-1);
QSort(ind2+1, finish);
}
//Индукция по размеру массива и истинность инварианта
//доказывает истинность постусловия в общем случае.
}
}// QuickSort
Приведу некоторые пояснения к этому доказательству. Задание предусловия и постусловия процедуры QSort достаточно очевидно - сортируемый массив должен быть не пустым, а после работы метода должен быть отсортированным. Важной частью обоснования является четкое введение трех множеств - S1, S2, S3 - и условий, накладываемых на их элементы. Эти условия и становятся частью инварианта, сохраняющегося при работе различных циклов нашего метода. Вначале множества S1 и S3 пусты, в ходе вычислений пустым становится множество S2. Так происходит формирование подзадач, к которым рекурсивно применяется алгоритм. Особым представляется случай, когда множество S1 тоже пусто. Нетрудно показать, что эта ситуация возможна только в том случае, если случайно выбранный элемент множества, служащий критерием разбиения исходного множества на два подмножества, является минимальным элементом.
Почему обоснование полезно практически? Дело в том, что в данном алгоритме приходится следить за границами множеств (чтобы они не пересекались), за пустотой множеств (служащих условием окончания циклов), за выполнением условий, накладываемых на элементы множеств. Если явно не ввести эти понятия, то вероятность ошибки существенно возрастает. В заключение следует все-таки привести результат сортировки хотя бы одного массива.
Рис. 10.3. Результаты быстрой сортировки массива
- 1. Лекция: Visual Studio .Net, Framework .Net
- Открытость
- Модульность
- Виртуальная машина
- Дизассемблер и ассемблер
- Метаданные
- Сборщик мусора - Garbage Collector - и управление памятью
- Исключительные ситуации
- События
- Общие спецификации и совместимые модули
- 2. Лекция: Язык c# и первые проекты
- Создание c#
- Виды проектов
- Консольный проект
- Windows-проект
- Начало начал - точка "большого взрыва"
- Выполнение проекта по умолчанию после "большого взрыва"
- Проект WindowsHello
- Общий взгляд
- Система типов
- Типы или классы? и типы, и классы
- Семантика присваивания
- Преобразование к типу object
- Примеры преобразований
- Семантика присваивания. Преобразования между ссылочными и значимыми типами
- Операции "упаковать" и "распаковать" (boxing и unboxing).
- 4. Лекция: Преобразования типов
- Где, как и когда выполняются преобразования типов?
- Преобразования ссылочных типов
- Преобразования типов в выражениях
- Преобразования внутри арифметического типа
- Преобразования и класс Convert
- Проверяемые преобразования
- Исключения и охраняемые блоки. Первое знакомство
- Опасные вычисления в охраняемых проверяемых блоках
- Опасные вычисления в охраняемых непроверяемых блоках
- Опасные преобразования и методы класса Convert
- 5. Лекция: Переменные и выражения
- Объявление переменных
- Время жизни и область видимости переменных
- Глобальные переменные уровня модуля. Существуют ли они в c#?
- Int X,y; //координаты точки
- Локальные переменные
- Глобальные переменные уровня процедуры. Существуют ли?
- Константы
- Выражения
- Приоритет и порядок выполнения операций
- Перегрузка операций
- Операции sizeof и typeof
- Как получить подробную информацию о классе?
- Статические поля и методы арифметических классов
- Логические операции
- Условное выражение
- Операция приведения к типу
- Присваивание
- Специальные случаи присваивания
- Определенное присваивание
- Еще раз о семантике присваивания
- Рассмотрим объявления:
- Класс Math и его функции
- Класс Random и его функции
- Блок или составной оператор
- If(выражение_1) оператор_1
- If(выражение1) if(выражение2) if(выражение3) ...
- Оператор switch
- Операторы break и continue
- Циклы While
- Цикл foreach
- Процедуры и функции - функциональные модули
- Процедуры и функции - методы класса
- Процедуры и функции. Отличия
- Описание методов (процедур и функций). Синтаксис
- Список формальных аргументов
- Тело метода
- Вызов метода. Синтаксис
- О соответствии списков формальных и фактических аргументов
- Вызов метода. Семантика
- Поля класса или функции без аргументов?
- Пример: две версии класса Account
- Функции с побочным эффектом
- Методы. Перегрузка
- 10. Лекция: Корректность методов. Рекурсия
- Корректность методов
- Инварианты и варианты цикла
- Рекурсия
- Рекурсивное решение задачи "Ханойские башни"
- Быстрая сортировка Хоара
- 11. Лекция: Массивы языка c#
- Общий взгляд
- Динамические массивы
- Многомерные массивы
- Массивы массивов
- Процедуры и массивы
- Класс Array
- Массивы как коллекции
- Сортировка и поиск. Статические методы класса Array
- Сводка свойств и методов класса Array
- Класс Object и массивы
- Массивы объектов
- Массивы. Семантика присваивания
- Общий взгляд
- Класс char[] - массив символов
- Операции над строками
- Строковые константы
- Неизменяемый класс string
- Статические свойства и методы класса String
- Метод Format
- Методы Join и Split
- Динамические методы класса String
- Операции над строками
- Основные методы
- Емкость буфера
- Пространство имен RegularExpression и классы регулярных выражений
- Немного теории
- Синтаксис регулярных выражений
- Классы Match и MatchCollection
- Классы Group и GroupCollection
- Пример "чет и нечет"
- Пример "око и рококо"
- Пример "кок и кук"
- Пример "обратные ссылки"
- Пример "Дом Джека"
- Пример "Атрибуты"
- 16. Лекция: Классы
- Синтаксис класса
- Поля класса
- Доступ к полям
- Методы-свойства
- Индексаторы
- Константы
- Конструкторы класса
- Деструкторы класса
- Проектирование класса Rational
- Методы класса Rational
- Закрытый метод нод
- Операции над рациональными числами
- Константы класса Rational
- Развернутые и ссылочные типы
- Классы и структуры
- Класс Rational или структура Rational
- Встроенные структуры
- Еще раз о двух семантиках присваивания
- Перечисления
- Персоны и профессии
- 18. Лекция: Отношения между классами. Клиенты и наследники
- Отношения между классами
- Отношения "является" и "имеет"
- Отношение вложенности
- Расширение определения клиента класса
- Отношения между клиентами и поставщиками
- Сам себе клиент
- Наследование
- Добавление полей потомком
- Конструкторы родителей и потомков
- Добавление методов и изменение методов родителя
- Статический контроль типов и динамическое связывание
- Три механизма, обеспечивающие полиморфизм
- Пример работы с полиморфным семейством классов
- Абстрактные классы
- Классы без потомков
- Преобразование к классу интерфейса
- Наследование от общего предка
- Клонирование и интерфейс iCloneable
- Сериализация объектов
- Класс с атрибутом сериализации
- Интерфейс iSerializable
- 20. Лекция: Функциональный тип в c#. Делегаты
- Как определяется функциональный тип и как появляются его экземпляры
- Функции высших порядков
- Вычисление интеграла
- Построение программных систем методом "раскрутки". Функции обратного вызова
- Наследование и полиморфизм - альтернатива обратному вызову
- Делегаты как свойства
- Операции над делегатами. Класс Delegate
- Пример "Плохая служба"
- 21. Лекция: События
- Классы с событиями
- Класс sender. Как объявляются события?
- Делегаты и события
- Как зажигаются события
- Классы receiver. Как обрабатываются события
- Классы с событиями, допустимые в каркасе .Net Framework
- Пример "Списки с событиями"
- Класс sender
- Классы receiver
- Переопределение значений аргументов события
- Классы с большим числом событий
- Проект "Город и его службы"
- 22. Лекция: Универсальность. Классы с родовыми параметрами
- Наследование и универсальность
- Синтаксис универсального класса
- Класс с универсальными методами
- Два основных механизма объектной технологии
- Стек. От абстрактного, универсального класса к конкретным версиям
- Ограниченная универсальность
- Синтаксис ограничений
- Список с возможностью поиска элементов по ключу
- Как справиться с арифметикой
- Родовое порождение класса. Предложение using
- Универсальные делегаты
- Framework .Net и универсальность
- 23. Лекция: Отладка и обработка исключительных ситуаций
- Корректность и устойчивость программных систем
- Жизненный цикл программной системы
- Искусство отладки
- Отладочная печать и условная компиляция
- Классы Debug и Trace
- Метод Флойда и утверждения Assert
- Выбрасывание исключений. Создание объектов Exception
- If !MyMethod(){// обработка ошибки}
- Параллельная работа обработчиков исключений
- Блок finally
- Класс Exception
- Организация интерфейса
- Форма и элементы управления
- Взаимодействие форм
- Модальные и немодальные формы
- Передача информации между формами
- Шаблон формы для работы с классом
- Наследование форм
- Два наследника формы TwoLists
- Огранизация меню в формах
- Создание меню в режиме проектирования
- Классы меню
- Создание инструментальной панели с командными кнопками
- Методы класса Graphics
- Класс Pen
- Класс Brush
- Событие Paint
- Кисти и краски
- 25. Лекция: Финальный проект
- Абстрактный класс Figure
- Класс Circle
- Список с курсором. Динамические структуры данных
- Классы элементов списка
- Организация интерфейса