1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 4 из 7

Блочная сортировка: линия в среднем на равномерных данных

Коротко

Блочная сортировка (bucket sort) раскладывает n чисел из [0, 1) по n корзинам - число x идёт в корзину номер ⌊x·n⌋, - сортирует каждую корзину вставками и склеивает корзины по порядку. Если числа распределены РАВНОМЕРНО, в каждую корзину попадает в среднем одно число, и вся сортировка стоит Θ(n) в среднем. Объяснение - через пары: два числа оказываются в одной корзине с вероятностью 1/n, значит одинаковых по корзине пар в среднем (n−1)/2, и только они дают работу сортировке вставками. Гарантия держится на допущении о данных: если числа скучены, все падают в несколько корзин, и цена вырождается в квадрат.

Если ты знаешь, почему блочная сортировка линейна в среднем на равномерных данных и что с ней делают скученные данные, листай до «Теперь сам».

Как думаешь, можно ли без подсчёта сортировать дробные числа за линию

Сортировка подсчётом и поразрядная требуют целых ключей из обозримого диапазона. А что, если ключи - дробные числа из [0, 1): доли, вероятности, нормированные оценки? Их значений бесконечно много, массив счётчиков не заведёшь.

Прежде чем читать дальше, попробуй угадать: если миллион чисел из [0, 1) разбросан равномерно, сколько из них в среднем попадёт в отрезок [0.5, 0.500001) длиной в одну миллионную? И что из этого следует для сортировки?

На что это похоже

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

Механизм: корзины, вставки, склейка

вход: a[0..n−1], все числа в [0, 1)

1. завести n пустых корзин
2. каждое x положить в корзину ⌊x · n⌋        Θ(n)
3. каждую корзину отсортировать вставками    сумма по корзинам
4. выписать корзины по порядку номеров       Θ(n)

Склейка корзин по порядку даёт отсортированный массив, потому что корзина i содержит числа из [i/n, (i+1)/n): всё, что в корзине i, меньше всего, что в корзине i+1. Сравнения нужны только внутри корзины.

Сколько стоит шаг 3? Сортировка вставками делает столько сдвигов, сколько в корзине инверсий - пар, стоящих в неправильном порядке. Инверсия возможна только между числами из ОДНОЙ корзины. Считаем пары:

пар всего:                          n·(n−1)/2
вероятность, что пара в одной корзине:  1/n
    (при равномерных данных второе число попадает
     в корзину первого с вероятностью 1/n)
пар в одной корзине в среднем:      n·(n−1)/2 · 1/n = (n−1)/2
из них инверсий - половина:         (n−1)/4

То есть в среднем меньше одной инверсии на элемент, и шаг 3 стоит Θ(n). Вся сортировка - Θ(n) в среднем.

Правило

Блочная сортировка кладёт x ∈ [0, 1) в корзину ⌊x·n⌋, сортирует корзины вставками и склеивает по порядку. На равномерных данных каждая пара попадает в одну корзину с вероятностью 1/n, поэтому пар-соседей по корзине в среднем (n−1)/2, инверсий - (n−1)/4, и сортировка стоит Θ(n) в среднем. Это среднее по ДАННЫМ, а не гарантия: на скученных данных корзины переполняются, и худший случай - Θ(n²). Граница n·log n не нарушена: элементы раскладываются по значению, а сравнения идут лишь внутри корзин.

Разбор: десять чисел по десяти корзинам

вход: 0.78  0.17  0.39  0.26  0.72  0.94  0.21  0.12  0.23  0.68
n = 10, корзина = первая цифра после запятой

корзина 1: 0.17 0.12   → вставками: 0.12 0.17   (1 сдвиг)
корзина 2: 0.26 0.21 0.23 → 0.21 0.23 0.26      (2 сдвига)
корзина 3: 0.39
корзина 6: 0.68
корзина 7: 0.78 0.72   → 0.72 0.78              (1 сдвиг)
корзина 9: 0.94
остальные пусты

склейка: 0.12 0.17 0.21 0.23 0.26 0.39 0.68 0.72 0.78 0.94

Четыре сдвига на десять чисел. Формула обещала в среднем (10−1)/4 = 2.25 - здесь вышло чуть больше, потому что три числа случайно легли в корзину 2. Ровно так выглядит «в среднем»: отдельный вход отклоняется, но не далеко.

Разбор: равномерные данные против скученных

Программа сортирует блочной сортировкой n равномерных чисел и n чисел вида x⁸ - они тоже из [0, 1), но сгрудились у нуля. Считаем сдвиги сортировки вставками внутри корзин.

blochnaya.py
import random


def bucket_sort(a):
    n = len(a)
    buckets = [[] for _ in range(n)]
    for x in a:
        buckets[int(x * n)].append(x)
    shifts = 0
    out = []
    for b in buckets:
        for i in range(1, len(b)):          # вставками внутри корзины
            x, j = b[i], i - 1
            while j >= 0 and b[j] > x:
                b[j + 1] = b[j]
                j -= 1
                shifts += 1
            b[j + 1] = x
        out.extend(b)
    return out, shifts


rng = random.Random(1)
for n in (2000, 4000, 8000, 16000):
    ravn = [rng.random() for _ in range(n)]
    kosye = [rng.random() ** 8 for _ in range(n)]
    r1, s1 = bucket_sort(ravn)
    r2, s2 = bucket_sort(kosye)
    assert r1 == sorted(ravn) and r2 == sorted(kosye)
    print(f"n={n}: равномерные {s1} сдвигов, перекошенные {s2}")
↳  выводpython3 blochnaya.py
n=2000: равномерные 475 сдвигов, перекошенные 154215
n=4000: равномерные 1008 сдвигов, перекошенные 502168
n=8000: равномерные 1974 сдвигов, перекошенные 1703677
n=16000: равномерные 3945 сдвигов, перекошенные 5586347

У равномерных сдвигов почти ровно n/4, как и обещал счёт пар: 2000/4 = 500, 16000/4 = 4000. При удвоении n они удваиваются - линия. У скученных при каждом удвоении n сдвигов становится больше в 3.3-3.4 раза, то есть рост почти квадратичный: половина чисел x⁸ меньше 0.004, и при n = 16000 все они толпятся в первых 63 корзинах из 16000.

Что ломается без этого

Блочную сортировку применяют к данным, не спросив, как они распределены, - и получают квадрат там, где ждали линию. Симптом узнаваемый: на тестовых случайных числах всё летает, на настоящих данных (суммы платежей, время отклика, размеры файлов - у всех них длинный хвост и толпа у малых значений) сортировка тормозит в сотни раз. Вторая ошибка - число корзин меньше n: при k корзинах в каждой окажется около n/k чисел, и вставки внутри стоят n²/k - при k = 10 это просто сортировка вставками, ускоренная в десять раз. Третья - забыть, что ⌊x·n⌋ при x = 1.0 даёт корзину n, которой нет: интервал должен быть полуоткрытым.

Зачем это в работе

Чистая блочная сортировка встречается редко, а её идея - постоянно. Внешняя сортировка больших файлов и параллельная сортировка на многих машинах начинаются с того же шага: разбить диапазон ключей на корзины и отправить каждую корзину на свой диск или свою машину. Там корзины выбирают не равными по ширине, а по ВЫБОРКЕ из данных - так, чтобы в каждую попало примерно поровну, и скученность перестаёт быть проблемой. Та же мысль стоит за гистограммой в базе данных: планировщик запросов делит значения столбца на корзины равной заполненности, а не равной ширины. Когда видишь «разбить на диапазоны и обработать каждый отдельно», первым делом спроси, как будут распределены данные по диапазонам.

Теперь сам

n = 1000 чисел из [0, 1), но все они лежат в [0, 0.01) и внутри этого отрезка распределены равномерно. Сколько корзин из тысячи окажутся непустыми и сколько в среднем инверсий получит сортировка вставками внутри корзин? Как починить, не меняя алгоритм?

Ответ. Числа из [0, 0.01) попадают в корзины ⌊x·1000⌋ = 0..9 - непустых всего 10, в каждой около 100 чисел. Внутри корзины порядок случаен, инверсий в среднем 100·99/4 ≈ 2475 на корзину, около 24750 на все - против (1000−1)/4 ≈ 250 на равномерных данных: в сто раз больше. Чинится растяжкой: найти минимум и максимум за один проход и класть x в корзину ⌊(x − min)/(max − min)·n⌋ (с отдельной корзиной для самого максимума). Тогда те же числа снова равномерно заполняют все тысячу корзин.

Главное

Блочная сортировка кладёт x ∈ [0, 1) в корзину ⌊x·n⌋, сортирует корзины вставками и склеивает. Линейна она в среднем и только на равномерных данных: пара попадает в одну корзину с вероятностью 1/n, пар-соседей в среднем (n−1)/2, инверсий (n−1)/4 - в программе ровно n/4 сдвигов. На скученных данных (x⁸) сдвиги растут почти квадратично. Это среднее по данным, а не гарантия - и вопрос «как распределены ключи» надо задать до выбора алгоритма.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.