Практика 25 сентября. Мастер-теорема, быстрая сортировка, нижняя оценка сортировок
Мастер-теорема через дерево рекурсии и три случая, быстрая сортировка, разбиения Хоара и Ломуто и их отличие, выбор опорного, асимптотика и память, неустойчивость, нижняя оценка n log n для сортировок сравнением, компараторы.
Практика после лекций про сортировку слиянием и быструю сортировку. Сначала — мастер-теорема: как по рекуррентному соотношению сразу получить асимптотику рекурсивного алгоритма и почему у слияния выходит . Потом подробный разбор быстрой сортировки: два способа разбиения, выбор опорного элемента, время и память. Разбиения Хоара и Ломуто на тестах и коллоквиумах путают особенно часто. В конце доказано, что никакая сортировка сравнениями не работает в худшем случае быстрее .
«В Хоаре опорный элемент — средний, в Ломуто — последний» — неверно. Опорный элемент можно выбрать как угодно в обеих схемах, выбор не связан со способом разбиения. Схемы отличаются только тем, как работают указатели (§2.4).
1. Мастер-теорема
1.1. Рекуррентное соотношение
Алгоритм «разделяй и властвуй» делит задачу размера на части, рекурсивно решает некоторые из них и что-то делает сам. Время его работы описывается соотношением
| Параметр | Смысл |
|---|---|
| сколько рекурсивных вызовов делается из одного вызова | |
| во сколько раз уменьшается размер задачи в вызове | |
| «полезная работа» самого вызова без рекурсии: разделить данные, собрать ответ |
Обычно — многочлен, и оценивают его старшей степенью: . Если не многочлен, её ограничивают подходящим многочленом; есть и общая версия теоремы, до неё курс дойдёт позже.
| Алгоритм | ||||
|---|---|---|---|---|
| Сортировка слиянием | слияние двух половин, | |||
| Бинарный поиск | одно сравнение, |
В слиянии обе половины сортируются рекурсивно, поэтому . В бинарном поиске массив тоже делится пополам, но дальше работаем только с одной половиной — . Параметр показывает, какая часть массива приходится на один вызов: в обоих случаях половина, .
1.2. Дерево рекурсии
Нарисуем вызовы деревом. Корень — задача размера , у каждого узла детей размера в раз меньше.
| Уровень | Узлов | Размер задачи | Работа на уровне |
|---|---|---|---|
Размер уменьшается в раз, пока не станет , поэтому уровней . Складываем работу по уровням: Это геометрическая прогрессия, и ответ зависит только от того, сравнима ли с единицей.
1.3. Три случая
, то есть . На каждом уровне делается одинаковая работа — от числа уровней она не зависит. Сумма — это единиц: Основание логарифма не пишут: , логарифмы по разным основаниям отличаются в константу раз.
, то есть . Работа убывает от корня к листьям, и самый «жирный» уровень — корень. Сумма ограничена константой: Даже если , сумма не больше — огромная, но константа, и асимптотику она не меняет.
, то есть . Работа растёт к листьям, самый тяжёлый уровень — нижний, и сумма с точностью до константы равна последнему слагаемому: Последнее равенство — логарифмирование: . Итак, .
| Условие | Где основная работа | |
|---|---|---|
| в корне | ||
| поровну на всех уровнях | ||
| в листьях |
То же самое удобно помнить как сравнение с : какое из двух чисел больше, та степень и побеждает, а при равенстве добавляется логарифм.
Примеры.
- Слияние: , — случай , .
- Бинарный поиск: — тоже равенство, .
- : — работа в корне, , .
- : — работа в листьях, . Третий случай может давать и , и , и дробные степени: при , , получается .
Вывод теоремы строгий: если скажут «докажите, я не верю» — вот он. Но на коллоквиуме достаточно сказать, что асимптотика слияния получается по мастер-теореме, и объяснить, что это случай . Кто сможет вывести три случая, как выше, — только выиграет.
2. Быстрая сортировка
2.1. Идея: работа на спуске
В сортировке слиянием работа делается на подъёме рекурсии: массив делится пополам без всякой работы, а сливаются уже отсортированные половины.
В быстрой сортировке наоборот, работа делается на спуске. Выбирается опорный элемент , и массив разбивается так, чтобы слева оказались элементы , а справа — элементы . Затем каждая часть сортируется рекурсивно. На подъёме делать ничего не нужно: части уже стоят в правильном порядке относительно друг друга.
Элементы, равные опорному, могут попасть в любую часть — это нормально. Рекурсия заканчивается на массивах из одного элемента: такой массив уже отсортирован.
2.2. Разбиение Хоара
Два равноправных указателя идут навстречу друг другу с двух концов:
- идёт слева направо и останавливается на элементе — он «не на своей половине»;
- идёт справа налево и останавливается на элементе ;
- если , элементы меняются местами, и оба указателя сдвигаются дальше;
- так продолжается, пока указатели не пересекутся.
После этого элементы с индексами от начала до не больше , а от до конца — не меньше .
Пример. Массив , опорный (взят из середины — только для наглядности).
| Шаг | стоп | стоп | Обмен | Массив после |
|---|---|---|---|---|
| 1 | (пропустили ) | |||
| 2 | (пропустили ) | (пропустили ) | ||
| 3 | (пропустили ) | без изменений |
Теперь , — указатели пересеклись. Слева , справа , и рекурсия запускается от и .
void quick_sort(vector<int>& a, int l, int r) {
if (l >= r) return;
int p = a[l + rand() % (r - l + 1)]; // опорный — любой, здесь случайный
int i = l, j = r;
while (i <= j) {
while (a[i] < p) i++; // стоп на элементе >= p
while (a[j] > p) j--; // стоп на элементе <= p
if (i <= j) swap(a[i++], a[j--]);
}
quick_sort(a, l, j); // a[l..j] <= p
quick_sort(a, i, r); // a[i..r] >= p
}2.3. Разбиение Ломуто
Указатели неравноправны и идут в одну сторону:
- — граница: всё левее меньше опорного;
- просто проходит по массиву;
- если , то меняется с , и граница сдвигается вправо.
В классической записи опорный сначала переставляют в конец, а после прохода ставят на место — на границу между частями.
Пример. Тот же массив и тот же опорный . Переставляем в конец: , .
| ? | Действие | Массив после | ||
|---|---|---|---|---|
| 0 | 7 | нет | — | |
| 1 | 1 | да | , | |
| 2 | 8 | нет | — | |
| 3 | 3 | да | , | |
| 4 | 6 | нет | — | |
| 5 | 4 | да | , | |
| 6 | 9 | нет | — | |
| 7 | 2 | да | , |
В конце меняем с опорным: . Пятёрка стоит на индексе — ровно там же, где в отсортированном массиве .
void quick_sort(vector<int>& a, int l, int r) {
if (l >= r) return;
swap(a[l + rand() % (r - l + 1)], a[r]); // опорный — в конец
int p = a[r];
int i = l; // a[l..i-1] < p
for (int j = l; j < r; j++)
if (a[j] < p) swap(a[i++], a[j]);
swap(a[i], a[r]); // опорный на своё место
quick_sort(a, l, i - 1);
quick_sort(a, i + 1, r);
}Студенты, сравнивавшие обе схемы бенчмарками, иногда получают, что Ломуто быстрее, хотя классически быстрее считается Хоар. Преподаватель посоветовал статью на Хабре о том, как Ломуто можно оптимизировать под современные процессоры, — в том числе поэтому его и изучают.
2.4. Чем отличаются Хоар и Ломуто
Вопрос «в чём разница между разбиениями Хоара и Ломуто» любят задавать на тестах и коллоквиуме: ответ короткий и сразу показывает, понимаете ли вы тему. Правильный ответ:
- разница только в принципе разбиения;
- в Хоаре два равноправных указателя идут с разных сторон и меняют местами элементы, стоящие не в своей половине;
- в Ломуто один указатель отвечает за границу элементов меньше опорного, другой проходит по массиву.
Следствие: в Ломуто опорный элемент после разбиения стоит на своём окончательном месте и в рекурсию больше не попадает. В Хоаре это не гарантировано: опорный может остаться внутри одной из частей и переехать позже.
«В Хоаре берём средний опорный, в Ломуто — последний». Это только привычка из учебных примеров: середина удобна на рисунке. В коде выше в обеих схемах опорный случайный, и обе корректны. Каждый год примерно половина людей отвечает на тесте именно так — и теряет балл.
2.5. Время работы
Лучший случай — разбиение каждый раз пополам. Это то же дерево, что у слияния: .
Средний случай — тоже , даже если разбиение неровное. Пусть массив каждый раз делится в отношении . Самая длинная ветвь — та, где каждый раз отрезается лишь десятая часть. Её глубина , то есть снова логарифм, только по другому основанию, а основание — константа. На каждом уровне в сумме обрабатывается не больше элементов, поэтому время .
Худший случай — опорным каждый раз оказывается минимум или максимум. Тогда массив делится на и , потом на и , и так далее: глубина рекурсии , а работа
2.6. Выбор опорного элемента
| Способ | Как | Проблема |
|---|---|---|
| Середина | легко построить тест, где в середине всегда минимум или максимум, — и получить | |
| Медиана из трёх | средний по величине из первого, среднего и последнего | лучше, но против неё тоже строят «плохие» тесты |
| Случайный | индекс, случайный в пределах | подобрать тест, где случайный элемент всегда крайний, практически нельзя |
Медиана из трёх: для массива, где первый элемент , средний , последний , опорным станет . Он гарантированно не минимум и не максимум массива, если только все три не равны.
Случайный опорный работает быстро: генератор псевдослучайных чисел выдаёт число за константное время. Настоящей случайности в компьютере нет, но для сортировки это неважно: чтобы опорный всё время оказывался крайним, должно очень сильно не повезти.
В Ломуто с условием все элементы, равные опорному, уходят вправо. На массиве из одинаковых чисел каждое разбиение даёт части и , и сортировка деградирует до при любом выборе опорного. Хоар в этом случае меняет равные элементы местами и делит массив примерно пополам.
2.7. Память и устойчивость
| Слияние | Быстрая сортировка | |
|---|---|---|
| Время, худший случай | ||
| Время, средний случай | ||
| Доп. память | — буфер для слияния | стек рекурсии: при разбиении примерно пополам, в худшем случае |
| Устойчивость | устойчива | неустойчива |
Память. Быстрой сортировке не нужен буфер: всё переставляется на месте, а на подъёме нечего сливать. Память уходит только на стек рекурсии, и её объём — это глубина рекурсии. У слияния стек тоже есть, но несопоставимо меньше буфера на элементов, поэтому память слияния — .
Устойчивость. Сортировка устойчива, если элементы с равными ключами сохраняют исходный порядок. Быстрая сортировка неустойчива: обмены переносят элементы через большие расстояния, и равные элементы могут поменяться местами. На каком-то конкретном массиве порядок равных может и сохраниться, но это не гарантировано — а устойчивость означает именно гарантию.
3. Нижняя оценка сортировок сравнением
3.1. Дерево решений
Сортировки вставками, слиянием и быстрая сравнивают элементы попарно и в зависимости от ответа что-то переставляют. Такие сортировки называются сортировками сравнением. Их все можно описать одной моделью, не вникая, рекурсивный алгоритм или итеративный.
Дерево решений.
- Узел — состояние алгоритма: текущий порядок элементов и все его переменные.
- В каждом узле алгоритм задаёт вопрос «?». В зависимости от ответа «вселенная расщепляется» на две ветки, и алгоритм переходит в одно из двух новых состояний.
- Лист — конец работы: алгоритм выдал ответ, то есть перестановку исходного массива, которая делает его отсортированным.
Работа алгоритма на конкретном входе — путь от корня к листу, а число сравнений — длина этого пути. Время в худшем случае не меньше высоты дерева.
3.2. Высота не меньше логарифма факториала
Каждая из перестановок входного массива требует своего ответа, поэтому листьев не меньше . У двоичного дерева высоты не больше листьев: самое низкое дерево — полное, где на каждом следующем уровне вдвое больше узлов. Значит,
Оценим с двух сторон.
- Сверху: , поэтому .
- Снизу: в произведении последние множителей не меньше , а остальные не меньше , поэтому и
Обе оценки — это с точностью до константы, значит, .
Теорема. Любая сортировка сравнением в худшем случае делает сравнений.
Поэтому сортировка слиянием асимптотически оптимальна среди сортировок сравнением, и никакая хитрость не даст, например, в худшем случае.
Числа: — перестановок , . Любой алгоритм сравнения на каком-то входе из элементов делает не меньше сравнений.
Оценка касается худшего случая, то есть высоты дерева. Отдельные листья могут быть близко к корню: сортировка вставками на уже отсортированном массиве делает сравнение, это .
3.3. Компараторы
Сортировкам сравнением не важно, что именно сортируется: достаточно уметь для двух объектов ответить, какой из них должен стоять левее. Математически это отношение линейного порядка, а в C++ — компаратор: функция, которая принимает два объекта и возвращает bool.
enum Color { Red, Orange, Yellow, Green, Blue, Indigo, Violet }; // порядок цветов радуги
bool by_rainbow(Color x, Color y) {
return x < y; // true, если x должен стоять левее y
}
sort(colors.begin(), colors.end(), by_rainbow);Так можно отсортировать числа, строки в лексикографическом порядке, цвета радуги и любые свои объекты — задав компаратор или перегрузив для типа оператор <. Любая сортировка сравнением работает с ними без изменений.
Если же сортируются целые числа (или объекты по целочисленному ключу), можно не сравнивать элементы, а использовать их значения напрямую. Такие сортировки обходят оценку — это линейные сортировки, следующая тема.
Частые ошибки
- «В Хоаре опорный средний, в Ломуто последний». Выбор опорного не зависит от схемы разбиения; различается только работа указателей.
- Путать, где делается работа. Слияние работает на подъёме рекурсии, быстрая сортировка — на спуске.
- Считать по числу частей, а не вызовов. В бинарном поиске массив делится на две части, но рекурсивный вызов один: , .
- Писать основание логарифма в асимптотике. и отличаются в константу раз — это одно и то же .
- Думать, что неровное разбиение портит асимптотику. При пропорции глубина всё равно логарифмическая. Плохо, когда разбиение даёт и .
- Брать опорный всегда из середины и считать это защитой. Против фиксированного выбора легко построить тест с .
- Говорить, что быстрая сортировка не использует доп. память. Буфера нет, но стек рекурсии — от до .
- Называть быструю сортировку устойчивой, потому что на примере порядок сохранился. Устойчивость — это гарантия для любого входа.
- Считать, что нижняя оценка касается всех сортировок. Она верна только для сортировок сравнением; для целых чисел есть линейные сортировки.
Мини-тренажёр
- Найдите асимптотику: .
- Найдите асимптотику: .
- Найдите асимптотику: .
- Сколько уровней у дерева рекурсии слияния для ?
- Выполните разбиение Хоара массива с опорным . Где остановились указатели?
- Выполните разбиение Ломуто массива с опорным (опорный уже первый — сначала переставьте его в конец).
- Какова глубина рекурсии быстрой сортировки, если опорным всегда оказывается максимум? А если разбиение всегда ?
- Медиана из трёх для массива — какой элемент станет опорным?
- Сколько листьев минимум у дерева решений для сортировки элементов и какова минимальная высота?
- Почему быстрая сортировка не противоречит нижней оценке, хотя в среднем тоже , а в худшем — ?
Ответы
- , , — работа в корне: .
- — работа в листьях: .
- — равенство: .
- уровней (размеры ).
- стоп на (), стоп на (): обмен, . Затем стоп на (), стоп на (): обмен, , , . Указатели пересеклись: , .
- (обменяли и ), . : , обмен сам с собой, . : — нет. : , — , . : — нет. Итог: — , тройка на своём месте.
- Максимум каждый раз: глубина , время . Пропорция : глубина , время .
- Первый , средний , последний — медиана .
- листа, — высота не меньше .
- Нижняя оценка говорит, что худший случай не может быть лучше . Быстрая сортировка в худшем случае даже хуже — , противоречия нет. Слияние достигает оценки.
Шпаргалка
| Понятие | Суть |
|---|---|
| Рекуррента | : — вызовов, — во сколько раз меньше задача, — работа вызова |
| Дерево рекурсии | уровень : узлов, работа , уровней |
| — работа в корне | |
| — слияние , бинпоиск | |
| — работа в листьях | |
| Быстрая сортировка | разбиение на и на спуске, на подъёме ничего |
| Хоар | два равноправных указателя навстречу, обмен неправильно стоящих |
| Ломуто | — граница элементов , — проход; опорный встаёт на своё место |
| Опорный | выбор не зависит от схемы; середина — уязвима, медиана из трёх, случайный |
| Время | лучший и средний (даже при ), худший |
| Память | слияние ; быстрая — стек … |
| Устойчивость | слияние устойчиво, быстрая — нет |
| Нижняя оценка | листьев высота |
| Компаратор | bool cmp(x, y) — сортировка сравнением работает для любого линейного порядка |
Актуальная версия: https://m3105.ru/notes/algoritmy-i-struktury-dannyh/praktika-25-sentyabrya-master-teorema-bystraya-sortirovka-nizhnyaya-otsenka-sort
Проверь себя
20 вопросов по материалу лекции. Результаты хранятся только в вашем браузере.
20 вопросов о мастер-теореме, разбиениях Хоара и Ломуто, выборе опорного элемента, времени и памяти быстрой сортировки и нижней оценке сортировок сравнением.
- 20 вопросов
- Результат виден сразу после каждого ответа
- Порядок вопросов случайный

Комментарии0
Пока никто ничего не написал.
Войдите, чтобы оставить комментарий