Сортировки без сравнений и сортировочные сети

Counting sort и radix sort; сортировочные сети в дополнительном модуле

01

Когда можно обойти 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. В оценке появятся оба параметра. Это честная плата за более сильное предположение о входе, а не опровержение границы из предыдущей лекции.

comparison sort: Ω(n log n) · counting sort: O(n+m)
Ограничение моделиВремя O(n+m) включает создание массива из m счётчиков. Если m огромно относительно n, такая сортировка невыгодна и по времени, и по памяти.

При выборе алгоритма проверяйте не только длину массива, но и диапазон значений. При n = 1000 и m = 10⁹ инициализация счётчиков уже дороже обычной сортировки. Сжатие координат обычно требует предварительно упорядочить разные ключи, поэтому само по себе не делает весь процесс линейным.

Начнём с самого простого случая: элементы — это сами целые ключи, и сохранять вместе с ними дополнительные данные не требуется.

Выберите метод с учётом диапазона, а не только длины.

Задания раздела

n=200000, баллы 0…100. Какой массив частот нужен?

n=1000, ключи 0…10⁹. Что выбрать без огромной таблицы частот?

Попыток: 0
02

Сортировка подсчётом

Этот раздел откроется после предыдущих проверок.

Продолжить: Когда можно обойти 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. Повторы увеличивают счётчик, а не размер диапазона.

3[0]
1[1]
3[2]
0[3]
1[4]

Для текущего числа выберите счётчик, который нужно увеличить.

Создадим массив count из m нулей. Для каждого входного x увеличим count[x]. После этого исходный порядок уже не важен: частоты полностью описывают, сколько экземпляров каждого числа должно попасть в ответ.

Counting sort для одних целых ключей
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
Сортировка подсчётом
3[0]
1[1]
4[2]
1[3]
0[4]
2[5]
3[6]
00102030405060708090100110120130140150160170180190200

Частичный результат

Пустой массив

Счётчики значений 0…20 обнулены.
Шаг 1 / 13

Инициализация счётчиков и просмотр диапазона требуют Θ(m), чтение входа и запись результата — Θ(n). Поэтому время равно Θ(n + m). В показанном варианте отдельно хранятся счётчики и выходной массив.

T = Θ(n + m), S = Θ(n + m) с выходным массивом

Отрицательные ключи тоже допустимы, если диапазон мал: используйте индекс x−minKey и m = maxKey−minKey+1. Препятствие — большой размах значений, а не знак числа.

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Ограниченные баллы экзамена

У 200 000 участников баллы от 0 до 100. Частоты и один проход по 101 значению дают O(n + 101). Для времени финиша в микросекундах до 10¹² такой массив счётчиков уже неприемлем — нужна другая структура ключа или сортировка.

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

Заполните частоты и отличите n от m.

Задания раздела

Для [3,1,3,0,1] сопоставьте ключу его частоту.

Чему равно n для этого входа?

Ключи допускаются от 0 до 3 включительно. Чему равно m?

Какой ответ восстанавливается по частотам?

Попыток: 0
03

Устойчивость и префиксные суммы

Этот раздел откроется после предыдущих проверок.

Продолжить: Когда можно обойти 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 не занимает ячеек.

Адреса — индексы в новом массиве. Они не равны самим ключам и не являются последними индексами групп.

start[0]=0, start[k]=Σ₍j<k₎ count[j]

Теперь пройдём вход слева направо. Запись с ключом 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:2[0]
B:0[1]
C:2[2]
D:1[3]
E:0[4]
F:2[5]
G:1[6]
·[0]
·[1]
·[2]
·[3]
·[4]
·[5]
·[6]
Начала групп: 0, 2, 4. Обход входа слева направо.
Шаг 1 / 8

Идентификаторы A, C, F сохраняют порядок внутри группы с ключом 2. Одних числовых счётчиков без переноса записей для этого недостаточно.

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

На примере с именами две записи с ключом 2 попадут в соседние ячейки в исходном порядке. В коде Python и Rust ниже переносится вся запись, а не только ключ. Можно реализовать и обход справа налево с концами групп, но направление обхода и правило изменения указателя должны согласовываться.

Устойчивый counting sort для записей

Ключ находится в диапазоне [0, m), метка отличает записи с одинаковым ключом. По частотам вычисляем начала групп и раскладываем записи слева направо, увеличивая адрес группы. Это Θ(n + m) времени и Θ(n + m) памяти. Для большого диапазона m метод может оказаться дороже сортировки сравнением.

Устойчивый counting sort для записей / Python
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)))
Ожидаемый вывод
B E D G A C F

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Сортировка по нескольким полям

Чтобы получить порядок сначала по группе, затем по баллу, устойчиво отсортируйте по второстепенному полю, потом по основному. Порядок баллов внутри одинаковой группы сохранится. Это та же причина, по которой LSD radix sort начинает с младших разрядов.

Устойчивость полезна не только для имён участников. Она позволяет накапливать порядок по нескольким признакам: следующий проход сохраняет работу предыдущего внутри одинаковых новых ключей. На этом построена поразрядная сортировка.

Напишите stable_by_key(records): записи [ключ,метка] имеют целые ключи 0…100. Верните записи в порядке ключа, сохранив исходный порядок равных. Реализуйте подсчёт частот, адреса начала групп и устойчивое распределение без sort или sorted. Для пустого списка верните [].

Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

Открыто 0 из 3

    5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

    Тест 1 stable_by_key([[2, "A"], [1, "B"], [2, "C"]])Ожидается: [[1, "B"], [2, "A"], [2, "C"]]
    Тест 2 stable_by_key([])Ожидается: []
    Тест 3 stable_by_key([[0, "X"], [0, "Y"]])Ожидается: [[0, "X"], [0, "Y"]]

    Дополнительные тесты: 2. Проверяются границы и повторы.

    Попыток: 0
    04

    Поразрядная сортировка

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Когда можно обойти n log n →

    Например, для [21, 13, 12, 23] устойчивый проход по единицам даст [21, 12, 13, 23]. Затем проход по десяткам даст [12, 13, 21, 23]. Внутри одинаковой десятки сохраняется порядок по единицам, установленный первым проходом.

    Два десятичных разряда: что разрушает неустойчивость?

    Вход [21,13,12,23]. До перехода на следующий разряд предскажите его результат.

    21[0]
    13[1]
    12[2]
    23[3]

    Исходные числа. Сначала устойчиво распределим по единицам.

    Если диапазон всего числа слишком велик для счётчиков, рассмотрим его цифры по отдельности. Запишем ключи в системе с основанием m: каждая цифра уже лежит в небольшом диапазоне 0…m−1. Схема LSD начинает с младшей цифры и движется к старшим.

    LSD radix sort
    for d = 0 .. k−1:
        A = stableCountingSort(A, key=(x // m**d) % m)
    return A

    Инвариант после j проходов: числа упорядочены по j младшим разрядам. Следующий устойчивый проход группирует их по новой цифре и сохраняет внутри группы порядок предыдущих разрядов. После обработки всех k цифр получаем порядок по всему ключу.

    T = Θ(k(n+m)); память = O(n+m)

    Для k цифр в системе с основанием m.

    LSD radix sort: от младших цифр к старшим
    170[0]
    45[1]
    75[2]
    90[3]
    802[4]
    24[5]
    2[6]
    66[7]
    Исходные неотрицательные целые.
    Шаг 1 / 4

    Цена одного прохода — Θ(n + m), а число проходов — k. Если ключ имеет w бит и за раз берём b бит, то m = 2ᵇ, k = ⌈w/b⌉. Большое основание уменьшает число проходов, но увеличивает таблицу счётчиков. Для 32-битных беззнаковых ключей байтовый вариант использует четыре прохода и 256 счётчиков.

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

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

    Radix sort по байтам u32

    Обрабатываем четыре байта от младшего к старшему. Каждый проход — устойчивый counting sort с основанием 256. Для фиксированной разрядности u32 время Θ(4(n + 256)); память Θ(n + 256). Python-пример намеренно ограничивает целые тем же диапазоном 0…2³²−1. Знаковые числа требуют отдельного преобразования ключа.

    Radix sort по байтам u32 / Python
    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]))
    Ожидаемый вывод
    [2, 24, 45, 66, 75, 90, 170, 802]

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Сортировка машинных идентификаторов

    При n = 10⁶ и 32-битных беззнаковых ключах четыре байтовых прохода дают предсказуемое число просмотров и небольшую таблицу из 256 счётчиков. Но нужен выходной буфер: миллион u32 занимает 4 МБ полезных данных, два массива — около 8 МБ без накладных расходов.

    Основной маршрут завершён: вы умеете считать частоты, устойчиво распределять записи и сохранять младшие разряды между проходами. Перейдите к ограничениям и практике. Следующие четыре главы — дополнительный модуль «Параллельные алгоритмы» с сохранёнными моделями сетей.

    Предскажите два десятичных прохода и проверьте устойчивость.

    Задания раздела

    Вход [21,13,12,23]. Упорядочите после устойчивого прохода по единицам.

    1. 13
    2. 23
    3. 21
    4. 12

    Теперь выполните устойчивый проход по десяткам.

    1. 21
    2. 12
    3. 13
    4. 23

    Внутри равных десяток второй проход развернул порядок и получил [13,12,23,21]. Что нарушено?

    Попыток: 0
    А—Я

    Справочник терминов

    Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.

    Общий справочник всех лекций ↗

    Найдено: 12 из 12

    Counting sort / Counting sortСортировка с подсчётом частот ключей.
    Устойчивость / StabilityСохранение относительного порядка равных ключей.
    Ключ / KeyПризнак сортировки записи.
    Префиксная сумма / Prefix sumСумма элементов последовательности до заданной позиции.
    Корзина / BucketДиапазон выходных позиций для конкретного ключа.
    Radix sort / Radix sortПоследовательная устойчивая сортировка по цифрам ключа.
    LSD / Least significant digitПорядок разрядов от младшего к старшему.
    Компаратор / Compare-exchangeОперация минимума/максимума пары.
    Сортировочная сеть / Sorting networkФиксированная последовательность компараторов.
    Глубина сети / Network depthЧисло последовательных параллельных уровней.
    Принцип 0–1 / Zero-one principleДля корректности сети достаточно проверить бинарные входы.
    Битоническая последовательность / Bitonic sequenceЦиклически сдвинутая последовательность «рост, затем спад».
    05

    Ограничения и выбор алгоритма

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Когда можно обойти n log n →

    Применим полученные оценки к максимальному входу. Для целочисленных сортировок отдельно запишите n, диапазон m и число разрядов k. Без этих параметров фраза «линейная сортировка» неполна: O(n + m) может оказаться хуже O(n log n).

    Сравниваем длину массива и диапазон ключей

    Проверьте n, m и число разрядов до выбора сортировки

    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 такой тест уже непрактичен. Отдельно обработайте ноль, максимальный ключ, пустой набор и неподходящую длину сети.

    n=200000, ключи 0…100. Сколько счётчиков достаточно для counting sort?

    Выберите один ответ
    Попыток: 0
    06

    Практика: 3 уровня

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Когда можно обойти n log n →

    Разминка перед Valid Anagram и Height Checker

    Сначала восстановите числа по частотам, затем устойчиво распределите целые записи. Для одинакового ключа проверяется исходный порядок меток. Извлечение байта u32 — дополнительная задача после десятичного radix sort.

    Напишите counting_sort(a,m): верните целые ключи в неубывающем порядке через массив частот. Гарантируется m ≥ 0, все 0 ≤ x < m, а при m = 0 вход пуст. Не используйте sort или sorted. Исходный a можно оставить неизменным; выход — новый список.

    Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

    Открыто 0 из 3

      5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

      Тест 1 counting_sort([3, 1, 3, 0, 1], 4)Ожидается: [0, 1, 1, 3, 3]
      Тест 2 counting_sort([], 0)Ожидается: []
      Тест 3 counting_sort([0, 0, 0], 1)Ожидается: [0, 0, 0]

      Дополнительные тесты: 3. Проверяются границы и повторы.

      Попыток: 0

      Базовый маршрут: Valid Anagram, Height Checker и собственный counting sort. H-Index — самостоятельное применение; Maximum Gap — необязательное испытание ★.

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

      Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.

      01

      Лёгкие

      Три задачи для закрепления основных приёмов.

      1. LeetCode · 242 · Easy

        Valid Anagram

        Заменить сортировку строк сравнением частот небольшого алфавита.

        Ограничения. Обе длины до 5 · 10⁴, только строчные английские буквы.

        Идея и оценка сложности

        26 счётчиков достаточно; время O(|s|+|t|+26), память O(26). Разные длины сразу исключают равенство мультимножеств. Дополнение условия о Unicode меняет модель алфавита и не решается теми же 26 ячейками.

      2. LeetCode · 1122 · Easy

        Relative Sort Array

        Выписать счётчики сначала в заданном порядке, затем по возрастанию.

        Ограничения. Длины до 1000; значения от 0 до 1000; элементы второго массива различны.

        Идея и оценка сложности

        Посчитайте частоты первого массива. Для каждого ключа второго выпишите его копии и обнулите счётчик, затем обойдите остаток диапазона. Время O(n+m+U), где U=1001. Не путайте заданный порядок ключей с устойчивостью записей.

      3. LeetCode · 1051 · Easy

        Height Checker

        Получить отсортированный эталон по частотам и сравнить позиции.

        Ограничения. До 100 ростов, каждый от 1 до 100.

        Идея и оценка сложности

        Счётчики восстанавливают ожидаемую последовательность за O(n+100). Сравнивайте её с исходным массивом, не уничтожив нужный порядок до подсчёта отличий. Малые ограничения позволяют также проверить решение обычной сортировкой в тесте.

      02

      Средние

      Две задачи на сочетание идей и анализ ограничений.

      1. LeetCode · 274 · Medium

        H-Index

        Сжать большие ключи до класса «не меньше n» и накопить частоты справа.

        Ограничения. До 5000 статей, число цитирований от 0 до 1000.

        Идея и оценка сложности

        Искомый h не больше n. Ключ min(citation,n) сохраняет нужную информацию; счётчиков n+1 достаточно. Обход справа поддерживает число статей с хотя бы h цитированиями. Время O(n), память O(n). Обоснуйте, почему сжатие не меняет ответ.

      2. LeetCode · 451 · Medium

        Sort Characters By Frequency

        Отсортировать по частоте, используя частоту как целочисленный ключ.

        Ограничения. Длина строки до 5 · 10⁵; латинские буквы обоих регистров и цифры.

        Идея и оценка сложности

        Посчитайте частоты и разложите символы в корзины с индексом частоты. Обход корзин в обратном порядке и построение ответа дают O(n+алфавит), но корзины требуют O(n) места. Для одинаковых частот порядок произволен; разные регистры считаются отдельно.

      03

      Тяжёлая

      Одна задача для переноса идеи на новую постановку.

      1. LeetCode · 164 · Medium; тяжёлая в этом курсе

        Maximum Gap

        Выполнить требование линейного времени при широком диапазоне ключей.

        Ограничения. До 10⁵ чисел от 0 до 10⁹; требуется линейное время и линейная дополнительная память.

        Идея и оценка сложности

        В курсе задача отнесена к тяжёлым ради самостоятельного линейного решения; на платформе она Medium. Используйте фиксированное число устойчивых radix-проходов либо докажите корзиночный подход. Counting sort с миллиардом ячеек не подходит. После сортировки достаточно максимума соседних разностей; при одном элементе ответ 0.

      Проверьте адреса групп и порядок записей.

      Задания раздела

      count=[1,2,0,2]. Где начинается группа ключа 3?

      Вход [(2,Z),(1,B),(2,A)]. Какой выход устойчив?

      n=200000, m=10⁹+1; счётчик u32. Только таблица частот требует примерно:

      Попыток: 0
      Дополнительные материалыДля углублённого разбора · не влияют на прохождение
      ДОП.

      Сортировочная сеть

      Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута counting sort и radix sort. Здесь исследуем заранее заданные сравнения и их параллельное выполнение.

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

      Компаратор (Comparator / compare-exchange) — Двухвходовая операция, направляющая минимум в один выход, максимум — в другой.

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

      Сортировочная сеть (Sorting network) — Фиксированная схема компараторов: пары сравнений определены заранее и не зависят от входных значений.

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

      Сеть чётно-нечётных обменов
      7[0]
      2[1]
      4[2]
      1[3]
      5[4]
      8[5]
      3[6]
      6[7]
      Начальный набор на восьми проводах.
      Шаг 1 / 9
      Глубина сети
      8
      Всего компараторов
      28

      Теперь нужно измерять две величины. Работа — общее число компараторов, а глубина — число последовательных слоёв. На обычном одном процессоре приходится выполнить всю работу; при достаточном числе исполнителей время может определяться глубиной.

      СхемаКомпараторыГлубина
      Нечётно-чётная перестановочная сортировкаΘ(n²)Θ(n)
      Битоническая сортировочная сетьΘ(n log² n)Θ(log² n)
      Оптимальные по глубине теоретические сетиO(n log n)O(log n), большие константы

      Фиксированная схема удобна для параллельного исполнения, но как доказать, что она сортирует любой вход? Следующий принцип сведёт проверку произвольных чисел к проверке только нулей и единиц.

      Какие компараторы можно выполнять в одном параллельном слое?

      Выберите один ответ
      Попыток: 0
      ДОП.

      Принцип нулей и единиц

      Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута 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 и останется нарушением порядка. Значит, сеть, ошибающаяся хотя бы на одном числовом входе, обязательно ошибается и на некотором двоичном.

      Контрапозиция
      Если выход сети на некотором произвольном входе содержит инверсию a>b, выберем t с b<t≤a. После порогового преобразования образуется инверсия 1,0. Значит, сеть не сортирует некоторый бинарный вход.

      Для восьми проводов существует всего 2⁸ = 256 двоичных входов. Модель позволяет проверить их все. Это полноценная проверка именно данной фиксированной сети; для произвольного n экспоненциальный перебор уже непрактичен и нужен общий довод о конструкции.

      Пороговое преобразование в нули и единицы
      9[0]
      2[1]
      7[2]
      1[3]
      8[4]
      4[5]
      6[6]
      3[7]

      Сначала порог, затем сеть:

      1[0]
      0[1]
      1[2]
      0[3]
      1[4]
      0[5]
      1[6]
      0[7]
      0[0]
      0[1]
      0[2]
      0[3]
      1[4]
      1[5]
      1[6]
      1[7]

      Сначала сеть, затем тот же порог:

      0[0]
      0[1]
      0[2]
      0[3]
      1[4]
      1[5]
      1[6]
      1[7]

      Результаты совпали: да. Один пример иллюстрирует принцип; он не заменяет доказательства для всех входов.

      Найдите двоичный контрпример для фиксированной сети

      Три провода. Сравнения заранее заданы: (0,1), (1,2), (0,1). Неисправная версия пропускает последний компаратор; схема не зависит от значений.

      1[0]
      1[1]
      0[2]

      После всех компараторов:

      1[0]
      0[1]
      1[2]

      Принцип 0–1 дал инструмент доказательства. Теперь построим сеть не перебором подходящих компараторов, а рекурсивно: сначала научимся объединять последовательности специальной формы.

      Сколько двоичных входов нужно проверить для фиксированной сети на 8 проводах?

      Выберите один ответ
      Попыток: 0
      ДОП.

      Битоническая последовательность и слияние

      Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута counting sort и radix sort. Здесь исследуем заранее заданные сравнения и их параллельное выполнение.

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

      Например, [1,4,7,9,8,6,3,2] сначала возрастает, затем убывает. В модели цветом выделен целый текущий слой: его компараторы используют непересекающиеся провода и могут выполняться одновременно. Четыре компаратора в слое — четыре операции работы, но один уровень зависимости.

      Битоническая последовательность (Bitonic sequence) — Последовательность, которая после циклического сдвига сначала не убывает, затем не возрастает.

      Для длины n = 2ᵏ сравним пары на расстоянии n/2 и отправим меньшие значения в первую половину. У битонического входа после этого все элементы первой половины не больше всех элементов второй, а обе половины снова битонические. Теперь их можно обрабатывать независимо тем же способом.

      Такое битоническое слияние использует один слой, затем слияния половин. Оно имеет log₂ n слоёв. Чтобы получить сортировку любого входа, сначала рекурсивно отсортируем одну половину по возрастанию, другую по убыванию, а затем применим слияние.

      Битоническая сеть
      7[0]
      2[1]
      4[2]
      1[3]
      5[4]
      8[5]
      3[6]
      6[7]
      Начальный набор на восьми проводах.
      Шаг 1 / 7
      Глубина сети
      6
      Всего компараторов
      24

      Две сортировки половин идут параллельно, поэтому к глубине меньшей задачи прибавляется только глубина слияния. Для n = 2ᵏ получаем 1 + 2 + … + k = k(k+1)/2 слоёв. На каждом полном слое n/2 компараторов, значит суммарная работа составляет Θ(n log² n).

      D(n)=D(n/2)+log₂n ⇒ D(n)=Θ(log²n)
      РазмерыНаиболее простая схема и визуализация здесь используют длины n=2ᵏ. Для произвольного n нужны дополнения конструкции или заполнение фиктивными экстремальными значениями.

      В примере Python и Rust ниже воспроизведена именно фиксированная сеть. Контракт требует длину, равную степени двойки; для другой длины можно дополнить вход фиктивными максимальными значениями и затем убрать их из результата.

      Битоническая сеть для степени двойки

      Пары компараторов задаются только индексами, а не значениями. Пример допускает пустой массив или длину, равную степени двойки. В последовательном исполнении выполняется O(n log² n) сравнений, глубина сети O(log² n) предполагает параллельное исполнение независимых пар.

      Битоническая сеть для степени двойки / Python
      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]))
      Ожидаемый вывод
      [1, 2, 3, 4, 5, 6, 7, 8]

      В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

      Маленькие фиксированные блоки

      Сети используют, когда важна одна и та же схема операций на небольшом блоке данных: специализированные векторные или аппаратные реализации. Для обычной сортировки большого массива последовательная сеть O(n log² n) может проиграть O(n log n), даже если её параллельная глубина мала.

      Битоническая конструкция показывает, почему малую глубину нельзя читать как малое последовательное время. Завершим лекцию, разделив эти оценки и сопоставив их с нижними границами.

      Для n=8, k=log₂n=3 какова глубина рассмотренной битонической сортировки?

      Выберите один ответ
      Попыток: 0
      ДОП.

      Параллелизм и нижние границы

      Дополнительный модуль «Параллельные алгоритмы». Эта глава не нужна для завершения основного маршрута counting sort и radix sort. Здесь исследуем заранее заданные сравнения и их параллельное выполнение.

      Для восьми проводов рассмотренная битоническая сеть имеет 6 слоёв по 4 независимых компаратора. Суммарно 24 сравнения; при четырёх исполнителях достаточно шести синхронных этапов, а на одном исполнителе выполняются все 24 сравнения.

      Сеть всё ещё сортирует сравнением, поэтому общее число её компараторов не может быть меньше Ω(n log n) в худшем случае. Параллельность не отменяет эту работу: она позволяет выполнять независимые части одновременно.

      Для глубины действует отдельное ограничение. После одного слоя значение на проводе может зависеть не более чем от двух исходных значений, после двух — от четырёх, после d — от 2ᵈ. На первом выходе должен оказаться минимум среди всех n входов, поэтому необходимо 2ᵈ ≥ n и d = Ω(log n).

      Наша битоническая сеть имеет глубину Θ(log² n): она проста и регулярна, но по глубине не достигает теоретического оптимума. Существуют сети с O(log n) слоями; их сложное устройство и большие константы ограничивают практическую пользу.

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

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

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

      Что происходит с работой сети при запуске на одном последовательном исполнителе?

      Выберите один ответ
      Попыток: 0

      ПРОДОЛЖИТЬ ОБСУЖДЕНИЕ

      Промпт для ИИ

      Скопируйте промпт с текстом лекции в чат с ИИ, чтобы обсудить непонятные места. В него входят дополнительные материалы, псевдокод и примеры на обоих языках.