Когда можно обойти n log n
Например, баллы участников лежат от 0 до 100. Вместо сравнений каждой пары кандидатов заведём 101 счётчик: в ячейке с индексом s будем хранить число участников с баллом s. Одним проходом по данным узнаем все частоты, затем просмотрим значения по возрастанию.
Пусть 200 000 участников получили баллы 0…100: достаточно 101 счётчика. При том же числе участников, но ключах 0…10⁹, таблица уже огромна. Выбор зависит и от числа записей, и от диапазона значений.
Ключ (Key) — Значение, по которому упорядочивают объект; сам объект может содержать и другие данные.
Нижняя граница порядка n log n относится к сортировке, которая узнаёт порядок только сравнениями. Счётчик с индексом, равным значению, использует дополнительное свойство входа: небольшой диапазон целых ключей. Подробное доказательство границы вынесено в углубление лекции 03.
Обозначим число элементов через n, а размер диапазона ключей — через m: 0 ≤ key < m. В оценке появятся оба параметра. Это честная плата за более сильное предположение о входе, а не опровержение границы из предыдущей лекции.
При выборе алгоритма проверяйте не только длину массива, но и диапазон значений. При n = 1000 и m = 10⁹ инициализация счётчиков уже дороже обычной сортировки. Сжатие координат обычно требует предварительно упорядочить разные ключи, поэтому само по себе не делает весь процесс линейным.
Начнём с самого простого случая: элементы — это сами целые ключи, и сохранять вместе с ними дополнительные данные не требуется.
Сортировка подсчётом
Этот раздел откроется после предыдущих проверок.
Продолжить: Когда можно обойти n log n →Пусть вход равен [3, 1, 3, 0, 1], а m = 4. Частоты будут [1, 2, 0, 2]. Выпишем 0 один раз, 1 два раза, 2 ни разу и 3 два раза — получим [0, 1, 1, 3, 3].
n = 5 входных чисел; m = 4 возможных ключа: 0, 1, 2, 3. Повторы увеличивают счётчик, а не размер диапазона.
Для текущего числа выберите счётчик, который нужно увеличить.
Создадим массив count из m нулей. Для каждого входного x увеличим count[x]. После этого исходный порядок уже не важен: частоты полностью описывают, сколько экземпляров каждого числа должно попасть в ответ.
count = [0] * m
for x in A: count[x] += 1
out = []
for key = 0 .. m−1:
repeat count[key] times: out.push(key)
return outЧастичный результат
Пустой массив
Инициализация счётчиков и просмотр диапазона требуют Θ(m), чтение входа и запись результата — Θ(n). Поэтому время равно Θ(n + m). В показанном варианте отдельно хранятся счётчики и выходной массив.
Отрицательные ключи тоже допустимы, если диапазон мал: используйте индекс x−minKey и m = maxKey−minKey+1. Препятствие — большой размах значений, а не знак числа.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Ограниченные баллы экзамена
У 200 000 участников баллы от 0 до 100. Частоты и один проход по 101 значению дают O(n + 101). Для времени финиша в микросекундах до 10¹² такой массив счётчиков уже неприемлем — нужна другая структура ключа или сортировка.
Частоты позволяют восстановить числа, но не записи об участниках: по одному баллу нельзя восстановить имя или исходный индекс. Для переноса целых записей потребуется заранее вычислить место каждой группы ключей.
Устойчивость и префиксные суммы
Этот раздел откроется после предыдущих проверок.
Продолжить: Когда можно обойти n log n →Пусть вместо чисел сортируются записи (балл, имя). Для входа [(2, Анна), (1, Борис), (2, Вера)] хотим получить [(1, Борис), (2, Анна), (2, Вера)]. Помимо порядка баллов, сохраним порядок Анны и Веры, у которых ключи совпадают.
Устойчивая сортировка (Stable sort) — Не меняет относительный порядок объектов с равными ключами.
Из частот вычислим, где начинается каждая группа в выходном массиве. Перед группой ключа k стоят все элементы с меньшими ключами, поэтому её начало равно сумме count[0] + … + count[k−1]. Эти суммы называются префиксными.
Для частот [1,2,0,2] группа нулей начинается в 0, единиц — в 1, двоек и троек — в 3. Двоек нет, поэтому их пустая группа не занимает места. Сначала вычислите эти адреса сами.
Частоты ключей 0, 1, 2, 3 равны [1,2,0,2]. Перед группой ключа 3 стоят один ноль и две единицы; пустая группа ключа 2 не занимает ячеек.
Адреса — индексы в новом массиве. Они не равны самим ключам и не являются последними индексами групп.
Теперь пройдём вход слева направо. Запись с ключом k положим в первую свободную ячейку группы k, затем увеличим её указатель. Группы не пересекаются, и внутри одной группы более ранняя запись всегда занимает более раннее место.
count[key] = число объектов с данным ключом
start[0] = 0
for k = 1 .. m−1: start[k] = start[k−1] + count[k−1]
for object in A (слева направо):
k = object.key
out[start[k]] = object
start[k] += 1Идентификаторы A, C, F сохраняют порядок внутри группы с ключом 2. Одних числовых счётчиков без переноса записей для этого недостаточно.
На примере с именами две записи с ключом 2 попадут в соседние ячейки в исходном порядке. В коде Python и Rust ниже переносится вся запись, а не только ключ. Можно реализовать и обход справа налево с концами групп, но направление обхода и правило изменения указателя должны согласовываться.
Устойчивый counting sort для записей
Ключ находится в диапазоне [0, m), метка отличает записи с одинаковым ключом. По частотам вычисляем начала групп и раскладываем записи слева направо, увеличивая адрес группы. Это Θ(n + m) времени и Θ(n + m) памяти. Для большого диапазона m метод может оказаться дороже сортировки сравнением.
def stable_count(records: list[tuple[int, str]], m: int):
assert m >= 0 and all(0 <= key < m for key, _ in records)
count = [0] * m
for key, _ in records:
count[key] += 1
start = [0] * m
for key in range(1, m):
start[key] = start[key - 1] + count[key - 1]
out = [(0, "")] * len(records)
for key, label in records:
out[start[key]] = (key, label)
start[key] += 1
return out
data = [(2,"A"), (0,"B"), (2,"C"), (1,"D"), (0,"E"), (2,"F"), (1,"G")]
print(" ".join(label for _, label in stable_count(data, 3)))fn stable_count(records: &[(usize, char)], m: usize) -> Vec<(usize,char)> {
assert!(records.iter().all(|&(key,_)| key < m));
let mut count = vec![0; m];
for &(key,_) in records { count[key] += 1; }
let mut start = vec![0; m];
for key in 1..m { start[key] = start[key-1] + count[key-1]; }
let mut out = vec![(0,' '); records.len()];
for &(key,label) in records {
out[start[key]] = (key,label);
start[key] += 1;
}
out
}
fn main() {
let data = [(2,'A'),(0,'B'),(2,'C'),(1,'D'),(0,'E'),(2,'F'),(1,'G')];
let names: Vec<String> = stable_count(&data,3).iter()
.map(|&(_,label)| label.to_string()).collect();
println!("{}", names.join(" "));
}B E D G A C F
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Сортировка по нескольким полям
Чтобы получить порядок сначала по группе, затем по баллу, устойчиво отсортируйте по второстепенному полю, потом по основному. Порядок баллов внутри одинаковой группы сохранится. Это та же причина, по которой LSD radix sort начинает с младших разрядов.
Устойчивость полезна не только для имён участников. Она позволяет накапливать порядок по нескольким признакам: следующий проход сохраняет работу предыдущего внутри одинаковых новых ключей. На этом построена поразрядная сортировка.
Поразрядная сортировка
Этот раздел откроется после предыдущих проверок.
Продолжить: Когда можно обойти n log n →Например, для [21, 13, 12, 23] устойчивый проход по единицам даст [21, 12, 13, 23]. Затем проход по десяткам даст [12, 13, 21, 23]. Внутри одинаковой десятки сохраняется порядок по единицам, установленный первым проходом.
Вход [21,13,12,23]. До перехода на следующий разряд предскажите его результат.
Исходные числа. Сначала устойчиво распределим по единицам.
Если диапазон всего числа слишком велик для счётчиков, рассмотрим его цифры по отдельности. Запишем ключи в системе с основанием m: каждая цифра уже лежит в небольшом диапазоне 0…m−1. Схема LSD начинает с младшей цифры и движется к старшим.
for d = 0 .. k−1:
A = stableCountingSort(A, key=(x // m**d) % m)
return AИнвариант после j проходов: числа упорядочены по j младшим разрядам. Следующий устойчивый проход группирует их по новой цифре и сохраняет внутри группы порядок предыдущих разрядов. После обработки всех k цифр получаем порядок по всему ключу.
Для k цифр в системе с основанием m.
Цена одного прохода — Θ(n + m), а число проходов — k. Если ключ имеет w бит и за раз берём b бит, то m = 2ᵇ, k = ⌈w/b⌉. Большое основание уменьшает число проходов, но увеличивает таблицу счётчиков. Для 32-битных беззнаковых ключей байтовый вариант использует четыре прохода и 256 счётчиков.
Как выбирать основание
Пример ниже ограничен беззнаковыми 32-битными значениями и использует выходной буфер. Для произвольно длинных чисел количество разрядов уже нельзя считать константой. Для знаковых машинных типов также нужно отдельно согласовать порядок отрицательных и неотрицательных значений.
Radix sort по байтам u32
Обрабатываем четыре байта от младшего к старшему. Каждый проход — устойчивый counting sort с основанием 256. Для фиксированной разрядности u32 время Θ(4(n + 256)); память Θ(n + 256). Python-пример намеренно ограничивает целые тем же диапазоном 0…2³²−1. Знаковые числа требуют отдельного преобразования ключа.
def radix_sort(values: list[int]) -> list[int]:
assert all(0 <= x < 2**32 for x in values)
a = values.copy()
for shift in range(0, 32, 8):
count = [0] * 256
for x in a: count[(x >> shift) & 255] += 1
start = [0] * 256
for d in range(1, 256): start[d] = start[d-1] + count[d-1]
out = [0] * len(a)
for x in a:
d = (x >> shift) & 255
out[start[d]] = x
start[d] += 1
a = out
return a
print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))fn radix_sort(values: &[u32]) -> Vec<u32> {
let mut a = values.to_vec();
for shift in (0..32).step_by(8) {
let mut count = [0_usize; 256];
for &x in &a { count[((x >> shift) & 255) as usize] += 1; }
let mut start = [0_usize; 256];
for d in 1..256 { start[d] = start[d-1] + count[d-1]; }
let mut out = vec![0; a.len()];
for &x in &a {
let d = ((x >> shift) & 255) as usize;
out[start[d]] = x;
start[d] += 1;
}
a = out;
}
a
}
fn main() {
println!("{:?}",radix_sort(&[170,45,75,90,802,24,2,66]));
}[2, 24, 45, 66, 75, 90, 170, 802]
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Сортировка машинных идентификаторов
При n = 10⁶ и 32-битных беззнаковых ключах четыре байтовых прохода дают предсказуемое число просмотров и небольшую таблицу из 256 счётчиков. Но нужен выходной буфер: миллион u32 занимает 4 МБ полезных данных, два массива — около 8 МБ без накладных расходов.
Основной маршрут завершён: вы умеете считать частоты, устойчиво распределять записи и сохранять младшие разряды между проходами. Перейдите к ограничениям и практике. Следующие четыре главы — дополнительный модуль «Параллельные алгоритмы» с сохранёнными моделями сетей.
Справочник терминов
Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.
Общий справочник всех лекций ↗
Найдено: 12 из 12
Ограничения и выбор алгоритма
Этот раздел откроется после предыдущих проверок.
Продолжить: Когда можно обойти n log n →Применим полученные оценки к максимальному входу. Для целочисленных сортировок отдельно запишите n, диапазон m и число разрядов k. Без этих параметров фраза «линейная сортировка» неполна: O(n + m) может оказаться хуже O(n log n).
Сравниваем длину массива и диапазон ключей
Counting sort: n + m = 200 101 условных действий; только счётчики u32 — 0.00 МиБ. Сортировка сравнением: n log₂n ≈ 3 521 929.
Байтовый radix: k(n + 256) = 801 024, 256 счётчиков и выходной буфер. Число проходов k должно покрывать всю длину ключа.
Диапазон умеренный: counting sort — подходящий кандидат. Проверьте также память входа и выходного буфера.
Это оценка масштаба, а не секунд. 4 байта относятся к машинному u32; Python int и список требуют больше памяти. Счётчики u32 должны вмещать частоты.
- n = 200 000 и ключи 0…100: 101 счётчик, время O(n + 101), небольшой дополнительный диапазон.
- n = 200 000 и ключи 0…10⁹: массив из миллиарда счётчиков u32 занимает около 4 ГБ полезных данных. Лучше анализировать radix sort или сортировку сравнением.
- При u32 и основании 256 требуется четыре прохода. Для n = 10⁶ один буфер ключей занимает 4 МБ, вход и выход — около 8 МБ без прочих данных.
Разрядность и стоимость индексации
Значение Python int не ограничено 32 битами. В приведённом radix-примере контракт явно ограничивает диапазон u32, иначе четырёх проходов недостаточно. Для строк переменной длины нужно определить порядок символов, обработку конца строки и длину ключа; переносить числовой шаблон без изменений нельзя.
Углубление: параллельная сеть на обычном процессоре
Для n = 1024 битоническая сеть имеет 55 слоёв и 28 160 компараторов. Глубина 55 не означает 55 последовательных операций: без параллельных исполнителей выполняется вся работа. На большой n сравните O(n log² n) сети с O(n log n) обычной сортировки.
Проверяйте устойчивость на помеченных записях, а не только на одинаковых числах. Для маленькой сети переберите все 2ⁿ двоичных входов: при n = 8 их 256, при n = 64 такой тест уже непрактичен. Отдельно обработайте ноль, максимальный ключ, пустой набор и неподходящую длину сети.
Практика: 3 уровня
Этот раздел откроется после предыдущих проверок.
Продолжить: Когда можно обойти n log n →Разминка перед Valid Anagram и Height Checker
Сначала восстановите числа по частотам, затем устойчиво распределите целые записи. Для одинакового ключа проверяется исходный порядок меток. Извлечение байта u32 — дополнительная задача после десятичного radix sort.
Базовый маршрут: Valid Anagram, Height Checker и собственный counting sort. H-Index — самостоятельное применение; Maximum Gap — необязательное испытание ★.
Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.
Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.
Лёгкие
Три задачи для закрепления основных приёмов.
- LeetCode · 242 · Easy
Valid Anagram
Заменить сортировку строк сравнением частот небольшого алфавита.
Ограничения. Обе длины до 5 · 10⁴, только строчные английские буквы.
Идея и оценка сложности
26 счётчиков достаточно; время O(|s|+|t|+26), память O(26). Разные длины сразу исключают равенство мультимножеств. Дополнение условия о Unicode меняет модель алфавита и не решается теми же 26 ячейками.
- LeetCode · 1122 · Easy
Relative Sort Array
Выписать счётчики сначала в заданном порядке, затем по возрастанию.
Ограничения. Длины до 1000; значения от 0 до 1000; элементы второго массива различны.
Идея и оценка сложности
Посчитайте частоты первого массива. Для каждого ключа второго выпишите его копии и обнулите счётчик, затем обойдите остаток диапазона. Время O(n+m+U), где U=1001. Не путайте заданный порядок ключей с устойчивостью записей.
- LeetCode · 1051 · Easy
Height Checker
Получить отсортированный эталон по частотам и сравнить позиции.
Ограничения. До 100 ростов, каждый от 1 до 100.
Идея и оценка сложности
Счётчики восстанавливают ожидаемую последовательность за O(n+100). Сравнивайте её с исходным массивом, не уничтожив нужный порядок до подсчёта отличий. Малые ограничения позволяют также проверить решение обычной сортировкой в тесте.
Средние
Две задачи на сочетание идей и анализ ограничений.
- LeetCode · 274 · Medium
H-Index
Сжать большие ключи до класса «не меньше n» и накопить частоты справа.
Ограничения. До 5000 статей, число цитирований от 0 до 1000.
Идея и оценка сложности
Искомый h не больше n. Ключ min(citation,n) сохраняет нужную информацию; счётчиков n+1 достаточно. Обход справа поддерживает число статей с хотя бы h цитированиями. Время O(n), память O(n). Обоснуйте, почему сжатие не меняет ответ.
- LeetCode · 451 · Medium
Sort Characters By Frequency
Отсортировать по частоте, используя частоту как целочисленный ключ.
Ограничения. Длина строки до 5 · 10⁵; латинские буквы обоих регистров и цифры.
Идея и оценка сложности
Посчитайте частоты и разложите символы в корзины с индексом частоты. Обход корзин в обратном порядке и построение ответа дают O(n+алфавит), но корзины требуют O(n) места. Для одинаковых частот порядок произволен; разные регистры считаются отдельно.
Тяжёлая
Одна задача для переноса идеи на новую постановку.
- LeetCode · 164 · Medium; тяжёлая в этом курсе
Maximum Gap
Выполнить требование линейного времени при широком диапазоне ключей.
Ограничения. До 10⁵ чисел от 0 до 10⁹; требуется линейное время и линейная дополнительная память.
Идея и оценка сложности
В курсе задача отнесена к тяжёлым ради самостоятельного линейного решения; на платформе она Medium. Используйте фиксированное число устойчивых radix-проходов либо докажите корзиночный подход. Counting sort с миллиардом ячеек не подходит. После сортировки достаточно максимума соседних разностей; при одном элементе ответ 0.
Дополнительные материалыДля углублённого разбора · не влияют на прохождение
Сортировочная сеть
Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута counting sort и radix sort. Здесь исследуем заранее заданные сравнения и их параллельное выполнение.
До сих пор очередные действия алгоритма могли зависеть от данных: например, QuickSort выбирает следующие подзадачи после разбиения. Сортировочная сеть заранее фиксирует, какие позиции сравниваются и в каком порядке. Значения определяют только результат каждого сравнения.
Компаратор (Comparator / compare-exchange) — Двухвходовая операция, направляющая минимум в один выход, максимум — в другой.
Изобразим позиции массива горизонтальными проводами, а compare-exchange — соединением двух проводов. Обычный восходящий компаратор отправляет меньшее значение на провод с меньшим индексом, большее — на другой. При необходимости направление можно обратить.
Сортировочная сеть (Sorting network) — Фиксированная схема компараторов: пары сравнений определены заранее и не зависят от входных значений.
Компараторы с непересекающимися проводами независимы и образуют один параллельный слой. В модели пройдите несколько слоёв и обратите внимание: пары позиций не меняются, даже если изменить входные числа.
Теперь нужно измерять две величины. Работа — общее число компараторов, а глубина — число последовательных слоёв. На обычном одном процессоре приходится выполнить всю работу; при достаточном числе исполнителей время может определяться глубиной.
| Схема | Компараторы | Глубина |
|---|---|---|
| Нечётно-чётная перестановочная сортировка | Θ(n²) | Θ(n) |
| Битоническая сортировочная сеть | Θ(n log² n) | Θ(log² n) |
| Оптимальные по глубине теоретические сети | O(n log n) | O(log n), большие константы |
Фиксированная схема удобна для параллельного исполнения, но как доказать, что она сортирует любой вход? Следующий принцип сведёт проверку произвольных чисел к проверке только нулей и единиц.
Принцип нулей и единиц
Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута counting sort и radix sort. Здесь исследуем заранее заданные сравнения и их параллельное выполнение.
У небольшого массива с различными числами уже n! перестановок, а возможных самих значений ещё больше. Для сети можно существенно упростить доказательство благодаря тому, что каждый её узел выполняет только min и max.
Принцип 0–1 (Zero-one principle) — Сеть сравнений сортирует произвольные ключи тогда и только тогда, когда сортирует все последовательности из 0 и 1.
Выберем порог t и заменим каждое значение меньше t на 0, а остальные — на 1. Пороговое преобразование согласуется с min и max: сначала сравнить числа, а потом заменить их битами — то же самое, что сначала заменить их битами и сравнить биты. Поэтому преобразование можно мысленно провести через всю сеть.
Если на числовом выходе есть инверсия a > b, выберем b < t ≤ a. Эта пара превратится в 1, 0 и останется нарушением порядка. Значит, сеть, ошибающаяся хотя бы на одном числовом входе, обязательно ошибается и на некотором двоичном.
Контрапозиция
Для восьми проводов существует всего 2⁸ = 256 двоичных входов. Модель позволяет проверить их все. Это полноценная проверка именно данной фиксированной сети; для произвольного n экспоненциальный перебор уже непрактичен и нужен общий довод о конструкции.
Сначала порог, затем сеть:
Сначала сеть, затем тот же порог:
Результаты совпали: да. Один пример иллюстрирует принцип; он не заменяет доказательства для всех входов.
Три провода. Сравнения заранее заданы: (0,1), (1,2), (0,1). Неисправная версия пропускает последний компаратор; схема не зависит от значений.
После всех компараторов:
Принцип 0–1 дал инструмент доказательства. Теперь построим сеть не перебором подходящих компараторов, а рекурсивно: сначала научимся объединять последовательности специальной формы.
Битоническая последовательность и слияние
Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута counting sort и radix sort. Здесь исследуем заранее заданные сравнения и их параллельное выполнение.
В сортировке слиянием мы умели объединять два возрастающих списка. Для сети удобнее немного другая заготовка: первая половина возрастает, вторая убывает. Вместе они образуют битоническую последовательность.
Например, [1,4,7,9,8,6,3,2] сначала возрастает, затем убывает. В модели цветом выделен целый текущий слой: его компараторы используют непересекающиеся провода и могут выполняться одновременно. Четыре компаратора в слое — четыре операции работы, но один уровень зависимости.
Битоническая последовательность (Bitonic sequence) — Последовательность, которая после циклического сдвига сначала не убывает, затем не возрастает.
Для длины n = 2ᵏ сравним пары на расстоянии n/2 и отправим меньшие значения в первую половину. У битонического входа после этого все элементы первой половины не больше всех элементов второй, а обе половины снова битонические. Теперь их можно обрабатывать независимо тем же способом.
Такое битоническое слияние использует один слой, затем слияния половин. Оно имеет log₂ n слоёв. Чтобы получить сортировку любого входа, сначала рекурсивно отсортируем одну половину по возрастанию, другую по убыванию, а затем применим слияние.
Две сортировки половин идут параллельно, поэтому к глубине меньшей задачи прибавляется только глубина слияния. Для n = 2ᵏ получаем 1 + 2 + … + k = k(k+1)/2 слоёв. На каждом полном слое n/2 компараторов, значит суммарная работа составляет Θ(n log² n).
В примере Python и Rust ниже воспроизведена именно фиксированная сеть. Контракт требует длину, равную степени двойки; для другой длины можно дополнить вход фиктивными максимальными значениями и затем убрать их из результата.
Битоническая сеть для степени двойки
Пары компараторов задаются только индексами, а не значениями. Пример допускает пустой массив или длину, равную степени двойки. В последовательном исполнении выполняется O(n log² n) сравнений, глубина сети O(log² n) предполагает параллельное исполнение независимых пар.
def bitonic_sort(values: list[int]) -> list[int]:
a = values.copy()
n = len(a)
assert n == 0 or n & (n - 1) == 0
k = 2
while k <= n:
j = k // 2
while j:
for i in range(n):
other = i ^ j
if other > i:
ascending = i & k == 0
if (a[i] > a[other]) if ascending else (a[i] < a[other]):
a[i], a[other] = a[other], a[i]
j //= 2
k *= 2
return a
print(bitonic_sort([7, 2, 4, 1, 5, 8, 3, 6]))fn bitonic_sort(values: &[i64]) -> Vec<i64> {
let mut a = values.to_vec();
let n = a.len();
assert!(n == 0 || n.is_power_of_two());
let mut k = 2;
while k <= n {
let mut j = k / 2;
while j > 0 {
for i in 0..n {
let other = i ^ j;
if other > i {
let swap = if i & k == 0 { a[i] > a[other] }
else { a[i] < a[other] };
if swap { a.swap(i,other); }
}
}
j /= 2;
}
k *= 2;
}
a
}
fn main() { println!("{:?}", bitonic_sort(&[7,2,4,1,5,8,3,6])); }[1, 2, 3, 4, 5, 6, 7, 8]
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Маленькие фиксированные блоки
Сети используют, когда важна одна и та же схема операций на небольшом блоке данных: специализированные векторные или аппаратные реализации. Для обычной сортировки большого массива последовательная сеть O(n log² n) может проиграть O(n log n), даже если её параллельная глубина мала.
Битоническая конструкция показывает, почему малую глубину нельзя читать как малое последовательное время. Завершим лекцию, разделив эти оценки и сопоставив их с нижними границами.
Параллелизм и нижние границы
Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута counting sort и radix sort. Здесь исследуем заранее заданные сравнения и их параллельное выполнение.
Для восьми проводов рассмотренная битоническая сеть имеет 6 слоёв по 4 независимых компаратора. Суммарно 24 сравнения; при четырёх исполнителях достаточно шести синхронных этапов, а на одном исполнителе выполняются все 24 сравнения.
Сеть всё ещё сортирует сравнением, поэтому общее число её компараторов не может быть меньше Ω(n log n) в худшем случае. Параллельность не отменяет эту работу: она позволяет выполнять независимые части одновременно.
Для глубины действует отдельное ограничение. После одного слоя значение на проводе может зависеть не более чем от двух исходных значений, после двух — от четырёх, после d — от 2ᵈ. На первом выходе должен оказаться минимум среди всех n входов, поэтому необходимо 2ᵈ ≥ n и d = Ω(log n).
Наша битоническая сеть имеет глубину Θ(log² n): она проста и регулярна, но по глубине не достигает теоретического оптимума. Существуют сети с O(log n) слоями; их сложное устройство и большие константы ограничивают практическую пользу.
В этой лекции мы изменяли два разных предположения. Сортировки подсчётом и по разрядам используют представление ключей, чтобы сократить работу. Сети фиксируют зависимости между сравнениями, чтобы выполнять часть работы параллельно. При выборе метода нужно явно указать, какое из этих условий есть в вашей задаче.
После справочника оценим диапазон ключей, память и число проходов по конкретным ограничениям. В следующей лекции будем использовать уже готовый порядок: он позволит быстро отбрасывать невозможные ответы при поиске.