Рандомизация, быстрая сортировка и выбор статистики

Трёхчастное разбиение, QuickSort и QuickSelect; доказательства в углублении

01

Рандомизированные алгоритмы

В прошлой лекции куча дала сортировку с гарантией O(n log n). Теперь рассмотрим другой способ делить задачу на части. Он приведёт и к сортировке, и к поиску одного нужного элемента, а для хорошей скорости мы сначала разрешим алгоритму делать случайный выбор.

Возьмём [1,2,3,4,5,6,7,8,9]. Опора 1 оставляет восемь больших элементов; опора 5 делит внешние части по четыре; случайная опора может дать любой из этих случаев. Сначала сравните реальные запуски на одном входе.

Один массив, три способа выбирать опору

Вход остаётся [1,2,3,4,5,6,7,8,9]. Предположите, при каком выборе появится самая длинная цепочка подзадач, затем сравните результаты.

Выбор pivotПервый pivot; размеры <, =, >Уровней с разбиениемОбработано элементовПроверок ключей
Середина по значению5; 4, 1, 432163
Крайний ключ1; 0, 1, 8844132
Случайный индекс6; 5, 1, 342575

Счёт относится к учебному quickTrace: три прохода filter, поэтому на каждый обработанный элемент приходятся три проверки ключа. Уровни считаются только для подзадач длины больше одного. Середина по индексу здесь совпадает с серединой по значению, потому что вход уже отсортирован.

Меняется ход вычисления, а результат всегда один: [1,2,3,4,5,6,7,8,9]. Случайный pivot иногда оказывается крайним; он улучшает среднее по запускам, но не гарантирует короткий каждый запуск.

Рандомизированный алгоритм (Randomized algorithm) — Алгоритм, который помимо входных данных использует случайные биты или случайный выбор.

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

Ожидаемое время — среднее по случайным решениям алгоритма на одном фиксированном входе. Верный результат получается в каждом запуске, но работа может различаться. Отдельный запуск всё ещё может оказаться неудачным.

Углубление: формальное ожидание
Среднее время по случайным решениям алгоритма для фиксированного входа. Зафиксируем входной массив и мысленно запустим алгоритм много раз с независимыми случайными решениями. Среднее число операций в этой модели — математическое ожидание времени. Затем можно выбрать самый неудобный вход данной длины и оценить ожидание уже для него. max₍|A|=n₎ E[T(A, random)] Худшее по входам ожидаемое время: сначала усреднение по случайности, потом максимум по входам. Это не «среднее время по случайным входным массивам». Распределение входов может быть любым; случайность создаёт сам алгоритм.

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

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

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

Сравните разные опоры на одном и том же входе.

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

Массив [1,2,3,4,5,6,7,8,9], pivot = 1. Сколько элементов попадёт в большую часть?

Тот же массив, pivot = 5. Сколько элементов в каждой внешней части?

Что меняется между запусками со случайной опорой?

Попыток: 0
02

Быстрая сортировка

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

Продолжить: Рандомизированные алгоритмы →

Сортировка слиянием сначала делила массив по индексам, а затем объединяла упорядоченные части. QuickSort делает основную работу до рекурсии: выбирает значение pivot и разбивает элементы на меньшие, равные и большие него.

Например, для [5, 2, 5, 8, 1] при pivot = 5 получим L = [2, 1], E = [5, 5], R = [8]. Любой элемент L должен стоять раньше любого элемента E, а E — раньше R. Поэтому достаточно отсортировать только L и R и соединить три части. Группа равных уже находится на своём месте.

Выполните partition3 вручную: pivot = 5
5[0]
2[1]
5[2]
8[3]
1[4]

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

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

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

Выберите группу для первого значения 5.

QuickSort с трёхчастным разбиением
quickSort(A):
    if length(A) <= 1: return A
    pivot = randomChoice(A)
    L = [x in A if x < pivot]
    E = [x in A if x == pivot]
    R = [x in A if x > pivot]
    return quickSort(L) + E + quickSort(R)

Разбиение массива длины n требует линейного прохода. Дальнейшая работа зависит от размеров частей. Если они близки к половинам, на каждом уровне суммарно обрабатывается O(n) элементов, а уровней O(log n). Если каждый раз отделяется только один элемент, размеры становятся n, n−1, n−2, … и сумма вырастает до Θ(n²).

QuickSort: дерево разбиений
Вызовов solve
15
Проверок ключей
93
Уровней с разбиением
5

Подзадача, глубина 0

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

< pivot

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

= pivot

9[0]

> pivot

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

Pivot = 9: 8 меньших, 1 равных, 0 больших.
Шаг 1 / 8

Вызовы включают пустые части и части из одного элемента. Учебная реализация делает три прохода filter: по одной проверке <, =, > на каждый элемент обрабатываемой части. Поэтому число проверок равно тройному числу просмотренных элементов.

Учебный генератор с фиксированным начальным seed делает первый показ воспроизводимым. «Новый запуск» меняет seed; это не криптографическая случайность.

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

СценарийВремя
Сбалансированные разбиения, различные ключиΘ(n log n)
Худший допустимый порядок опорных элементовΘ(n²)
Случайный pivot, ожидание для различных ключейΘ(n log n)

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

Трёхчастный QuickSort

Равные pivot элементы уходят в отдельный блок и больше не рекурсируют. Это предотвращает цепочку одинаковых значений. Здесь используются дополнительные массивы: реализация не in-place. Для воспроизводимости применён простой генератор с seed; теоретическая ожидаемая O(n log n) формулируется для равномерных независимых выборов pivot, худшая оценка остаётся O(n²).

Трёхчастный QuickSort / Python
def quick_sort(values: list[int], seed: int = 1) -> list[int]:
    def solve(a: list[int]) -> list[int]:
        nonlocal seed
        if len(a) < 2:
            return a.copy()
        seed = (seed * 1664525 + 1013904223) & 0xffffffff
        pivot = a[seed % len(a)]
        left, equal, right = [], [], []
        for x in a:
            if x < pivot: left.append(x)
            elif x > pivot: right.append(x)
            else: equal.append(x)
        return solve(left) + equal + solve(right)
    return solve(values)

print(quick_sort([7, 2, 9, 4, 2, 1]))
Ожидаемый вывод
[1, 2, 2, 4, 7, 9]

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

Сортировка входа с большим числом повторов

При n ≤ 200 000 нельзя полагаться на «случайные тесты». Проверьте отсортированный вход, обратный порядок и все одинаковые элементы. Если нужна жёсткая гарантия времени, используйте алгоритм с гарантированной оценкой либо гибрид с ограничением глубины; рандомизация даёт ожидаемую, а не детерминированную гарантию.

QuickSort корректен при любом выборе pivot. Для оценки скорости осталось объяснить, почему случайный выбор обычно не позволяет дереву рекурсии вытянуться в длинную цепочку.

Проверьте смысл трёх групп перед рекурсией.

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

Распределите значения [5,2,5,8,1] при pivot = 5.

Какие группы рекурсивно сортирует QuickSort?

Попыток: 0
03

Почему случайный pivot помогает

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

Продолжить: Рандомизированные алгоритмы →

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

Один массив, три способа выбирать опору

Вход остаётся [1,2,3,4,5,6,7,8,9]. Предположите, при каком выборе появится самая длинная цепочка подзадач, затем сравните результаты.

Выбор pivotПервый pivot; размеры <, =, >Уровней с разбиениемОбработано элементовПроверок ключей
Середина по значению5; 4, 1, 432163
Крайний ключ1; 0, 1, 8844132
Случайный индекс6; 5, 1, 342575

Счёт относится к учебному quickTrace: три прохода filter, поэтому на каждый обработанный элемент приходятся три проверки ключа. Уровни считаются только для подзадач длины больше одного. Середина по индексу здесь совпадает с серединой по значению, потому что вход уже отсортирован.

Меняется ход вычисления, а результат всегда один: [1,2,3,4,5,6,7,8,9]. Случайный pivot иногда оказывается крайним; он улучшает среднее по запускам, но не гарантирует короткий каждый запуск.

Для анализа сначала предположим, что все ключи различны, и расположим их мысленно по возрастанию. Опорный элемент из центральной трети оставляет с каждой стороны не больше примерно 2n/3 элементов. Назовём такое разбиение удачным: оно не обязано быть ровно пополам, достаточно отсечь постоянную долю.

Углубление: центральная треть
Равномерный выбор попадает в центральную треть с вероятностью порядка 1/3 независимо от исходного порядка массива. Число попыток до удачного выбора имеет постоянное математическое ожидание. Даже если неудачные шаги почти не уменьшают задачу, после O(log n) удачных сокращений любой путь достигает малого размера. Отсюда ожидаемая длина пути для элемента равна O(log n). Каждый элемент участвует в линейных разбиениях вдоль своего пути, поэтому суммарная ожидаемая работа ограничена O(n log n). Но вероятность длинной серии неудач не равна нулю: оценка худшего случая остаётся квадратичной.
E[T(n)] = O(n log n), max T(n) = O(n²)

Ожидаемая и худшая оценки относятся к разным характеристикам.

Где хорошие pivot?
Меньше pivot
14
Больше pivot
15

Pivot лежит в средней трети: ни одна часть не забирает почти весь массив.

Для наглядности значения различны. Равномерный выбор позиции даёт постоянную вероятность попасть в среднюю треть рангов.

Повторы требуют отдельного внимания. Если все значения равны, двухчастное разбиение может удалять из рассмотрения только один элемент за шаг. Разделение на < pivot, = pivot, > pivot сразу исключает всю группу равных и сохраняет прогресс.

Исправление для повторовЕсли разделить только на «меньше pivot» и «больше pivot», равные элементы могут потеряться или возникнет отсутствие прогресса. Трёхчастное разбиение <, =, > корректно обрабатывает повторы.

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

Различайте удачное сокращение и гарантию для каждого запуска.

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

Всегда выбираем минимум отсортированной подзадачи. Как меняется её размер?

Случайный pivot исключает квадратичный отдельный запуск?

Попыток: 0
04

Разбиение внутри массива

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

Продолжить: Рандомизированные алгоритмы →

Сначала сохраним уже понятные три группы без дополнительных списков. Возьмём pivot = 5. Слева будут меньшие, затем равные, посередине ещё неизвестные, справа большие. После каждого обмена все обработанные области должны оставаться правильными.

Трёхчастное разбиение на месте: четыре области

lt — конец меньших; i — первый непроверенный; gt — начало больших. Интервалы имеют правую исключённую границу: [0,lt), [lt,i), [i,gt), [gt,n).

lt = 0; i = 0; gt = 5; pivot = 5.

5[0]
2[1]
5[2]
8[3]
1[4]
< 5

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

= 5

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

Не проверено
5[0]
2[1]
5[2]
8[3]
1[4]
> 5

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

Смотрите на a[i]. Какая команда сохранит все четыре области?

Трёхчастное разбиение на месте
lt = i = 0
gt = len(a)
while i < gt:
    if a[i] < pivot:
        a[lt], a[i] = a[i], a[lt]
        lt += 1
        i += 1
    elif a[i] == pivot:
        i += 1
    else:
        gt -= 1
        a[i], a[gt] = a[gt], a[i]
        # a[i] ещё не проверен: i не увеличиваем

После цикла [0,lt) меньше, [lt,gt) равны, [gt,n) больше pivot.

Теперь для сравнения рассмотрим двухчастный вариант с pivot в конце. В нём другой инвариант: равные не выделяются в отдельную готовую часть. На одинаковых элементах такой вариант может давать плохое сокращение. Не смешивайте его указатели i,j с lt,i,gt предыдущей реализации.

Разбиение можно выполнить обменами на полуинтервале A[l:r). Рассмотрим простой вариант: перенесём pivot в последнюю ячейку и будем просматривать остальные элементы слева направо.

Перед обработкой позиции j поддерживаем четыре области: A[l:i) содержит значения меньше pivot, A[i:j) — не меньше него, A[j:r−1) ещё не просмотрена, а A[r−1] хранит сам pivot. Если очередной элемент меньше pivot, меняем его с A[i] и увеличиваем i. В любом случае j продвигается вправо.

Двухчастное разбиение с pivot в конце
pivot = A[r−1]
i = l
for j = l .. r−2:
    if A[j] < pivot:
        swap(A[i], A[j]); i += 1
swap(A[i], A[r−1])
return i

В результате pivot стоит на позиции i; остальные элементы могут оставаться неотсортированными.

После прохода неизвестная область пуста. Обмен A[i] с последним элементом ставит pivot на итоговую позицию: слева все значения меньше него, справа — не меньше. Части ещё не отсортированы, но уже известно, какую из них обрабатывать дальше.

Partition: инвариант двух областей
7[0]
2[1]
9[2]
1[3]
5[4]
4[5]
8[6]
3[7]
6[8]
Pivot = 5. Префикс меньших элементов пока пуст.
Шаг 1 / 10

Порядок внутри областей не гарантируется. Здесь строится граница < pivot / ≥ pivot, сам pivot отдельно на границу не ставится.

Этот двухчастный вариант удобен для доказательства инварианта, однако множество равных значений может дать несбалансированную рекурсию. В примере Python и Rust ниже показано трёхчастное разбиение: оно дополнительно выделяет готовый блок равных. Само разбиение работает за O(n) времени и O(1) дополнительной памяти.

Стек вызовов
In-place версия экономит память на копирование массивов, но рекурсия всё ещё занимает стек. Оптимизация меньшей ветви в рекурсию, а большей — циклом, ограничивает глубину стека O(log n), даже если разбиения неудачны.

Partition без дополнительного массива

Поддерживаем [0, boundary) < pivot и [boundary, j) ≥ pivot. Каждый просмотр расширяет разобранный префикс. После прохода возвращаем длину левой области. Время Θ(n), дополнительная память Θ(1); устойчивость не гарантируется.

Partition без дополнительного массива / Python
def partition(a: list[int], pivot: int) -> int:
    boundary = 0
    for j in range(len(a)):
        if a[j] < pivot:
            a[boundary], a[j] = a[j], a[boundary]
            boundary += 1
    return boundary

data = [7, 2, 9, 1, 5, 4]
boundary = partition(data, 5)
print(data)
print(boundary)
Ожидаемый вывод
[2, 1, 4, 7, 5, 9]
3

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

Разделить участников по условию

Иногда нужно лишь вынести записи с подходящим свойством в начало: чётные значения, результаты ниже порога, активные заявки. Тогда полный порядок не нужен; один проход за O(n) достаточен. Если требуется исходный порядок внутри групп, выбирайте устойчивый вариант.

Разбиение сообщает больше, чем только две группы: оно показывает, сколько элементов меньше pivot и сколько ему равны. Этой информации достаточно, чтобы найти элемент заданного ранга, продолжая лишь одну ветвь рекурсии.

Допишите split3(a, pivot): верните [L,E,R] с элементами меньше, равными и большими pivot; внутри каждой группы сохраните исходный порядок. Используйте один проход без сортировки. Это понятная версия с O(n) дополнительной памятью; in-place вариант с четырьмя зонами исследуется отдельно.

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

Открыто 0 из 3

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

    Тест 1 split3([5, 2, 5, 8, 1], 5)Ожидается: [[2, 1], [5, 5], [8]]
    Тест 2 split3([], 3)Ожидается: [[], [], []]
    Тест 3 split3([2, 2], 2)Ожидается: [[], [2, 2], []]

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

    Попыток: 0
    05

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

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

    Продолжить: Рандомизированные алгоритмы →

    В рейтинге [7,2,9,4,4] нужен только третий результат по возрастанию — число 4 с индексом k = 2. Зачем полностью сортировать всех участников? Разбиение уже показывает, какие места занимают группы; нужную группу достаточно продолжить одну.

    k-я порядковая статистика (Order statistic) — Элемент с индексом k в мысленно отсортированном массиве; индексация с нуля.

    Индекс k считаем с нуля, поэтому минимум имеет ранг 0, максимум — n−1. Для [7, 2, 9, 4, 4] статистика с k = 2 равна 4. При чётной длине под медианой нужно заранее договориться понимать одну из двух центральных статистик или их среднее.

    Выполним такое же разбиение L, E, R, как в QuickSort. Если k < |L|, ответ лежит слева. Если |L| ≤ k < |L|+|E|, ответ равен pivot. Иначе переходим вправо и уменьшаем локальный ранг на |L|+|E|.

    QuickSelect: выберите одну ветвь и пересчитайте k

    Нужно значение с k = 3; текущий pivot = 9. Ранги начинаются с нуля.

    7[0]
    2[1]
    9[2]
    4[3]
    4[4]
    7[0]
    2[1]
    4[2]
    4[3]
    9[0]

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

    Ранг 3 означает четвёртый элемент по возрастанию. Выберите часть и её локальный ранг.

    QuickSelect
    select(A, k):
        pivot = randomChoice(A)
        L, E, R = split3(A, pivot)
        if k < len(L): return select(L, k)
        if k < len(L) + len(E): return pivot
        return select(R, k − len(L) − len(E))
    Локальный и глобальный индексыПри переходе в правую подзадачу нужно вычесть размер левой части и группы равных pivot, если k задаётся относительно текущей подзадачи.
    QuickSelect: продолжаем только в одной части

    Подзадача, глубина 0

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

    < pivot

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

    = pivot

    9[0]

    > pivot

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

    Нужен индекс 3 в текущей части; pivot = 9.
    Шаг 1 / 5

    QuickSelect продолжает только одну ветвь. При удачных сокращениях объём работы складывается как n + 2n/3 + (2/3)²n + … = O(n). Ожидаемое число попыток до очередного удачного сокращения постоянно, поэтому при случайном pivot ожидаемое время линейно. Цепочка крайне неудачных выборов всё ещё даёт Θ(n²).

    E[T(n)] = O(n), T_worst(n) = O(n²)

    В каждом шаге обрабатывается только одна рекурсивная ветвь.

    QuickSort сортирует обе внешние части, QuickSelect продолжает только одну; равные опоре сразу дают готовый диапазон рангов. В основном маршруте дальше сравним, какой метод нужен вашей задаче. Детерминированный выбор и доказательство нижней границы доступны отдельно.

    Напишите kth(a,k) по схеме QuickSelect: верните элемент ранга k, где 0 ≤ k < len(a), считая с нуля по возрастанию. Разбивайте на меньшие, равные и большие; продолжайте только в одной части. Не используйте sort или sorted. Учебная версия может создавать списки.

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

    Открыто 0 из 3

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

      Тест 1 kth([7, 2, 9, 4, 4], 2)Ожидается: 4
      Тест 2 kth([8], 0)Ожидается: 8
      Тест 3 kth([-3, 5, -1], 2)Ожидается: 5

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

      Попыток: 0
      06

      Три алгоритма рядом

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

      Продолжить: Рандомизированные алгоритмы →

      Сначала спросите: нужен весь порядок или один ранг? Затем: достаточно среднего по запускам или нужна гарантия худшего случая? Для полной сортировки дополнительно проверьте устойчивость и память.

      Сначала цель, затем алгоритм

      Одна статистика: QuickSelect, ожидаемо O(n), в худшем O(n²).

      У учебного QuickSort отдельные списки. Слово «QuickSort» само по себе не обещает in-place реализацию или постоянный размер стека.

      Общая операция у рассмотренных методов — разбиение относительно pivot. Различаются две вещи: сколько частей мы продолжаем обрабатывать и как выбираем опорный элемент. Эти решения определяют и результат, и оценку времени.

      МетодОжидаемое / гарантированноеСортирует всё?Память
      QuickSort со случайным pivotE[Θ(n log n)]; худшее Θ(n²)ДаO(log n) ожидаемый стек при in-place
      QuickSelectE[Θ(n)]; худшее Θ(n²)Нет, один k-й элементO(1) дополнительной помимо стека при in-place
      Медиана медиан (углубление)Гарантированное O(n)Нет, один k-й элементЗависит от реализации

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

      Гарантия против практической скоростиДетерминированная линейная селекция имеет сильную гарантию, но заметные константы. Рандомизированный QuickSelect на практике часто проще и быстрее; выбор зависит от требований к гарантированному времени.

      При чтении кода также различайте память разбиения и память всей программы. Обмены могут требовать O(1) дополнительных ячеек, но рекурсивные вызовы занимают стек. Примеры со списками L, E, R явно выделяют новые массивы и служат понятной реализацией идеи, а не обещанием минимального расхода памяти.

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

      Нужен один элемент заданного ранга; достаточно ожидаемого линейного времени. Что соответствует задаче?

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

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

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

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

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

      Рандомизированный алгоритм / Randomized algorithmИспользует случайность внутри вычисления.
      Математическое ожидание / ExpectationСреднее по возможным случайным исходам.
      Pivot / PivotОпорный элемент разбиения.
      Partition / PartitionПерестройка по отношению к опорному элементу.
      QuickSort / Quick sortРекурсивная сортировка разбиением.
      QuickSelect / Selection algorithmПоиск одного элемента заданного ранга.
      Порядковая статистика / Order statistick-й элемент отсортированной версии массива.
      Медиана / MedianЦентральная порядковая статистика.
      Медиана медиан / Median of mediansДетерминированный выбор хорошего опорного элемента по группам.
      BFPRT / Blum–Floyd–Pratt–Rivest–TarjanДетерминированный алгоритм линейной селекции.
      Дерево решений / Decision treeДвоичная модель результатов сравнений.
      Нижняя граница / Lower boundНевозможность в данной модели работать асимптотически быстрее указанного порядка.
      Трёхчастное разбиение / Three-way partitionГруппировка элементов меньше, равных и больше опорного.
      07

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

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

      Продолжить: Рандомизированные алгоритмы →

      При выборе между разобранными методами сначала определите, нужен ли полный порядок или только один ранг. Для n ≤ 200 000 сортировка с ожидаемой O(n log n) и выбор с ожидаемой O(n) решают разные объёмы работы; O(n²) на неудачном разбиении означает десятки миллиардов просмотров.

      Ожидаемое время и максимальная глубина

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

      Считаем память реализации

      Выделение левой и правой частей проще in-place кода, но копирование и рекурсия расходуют память. На цепочке очень несбалансированных разбиений накопленные копии могут потребовать O(n²) пикового хранения. Для воспроизводимых тестов фиксируйте seed; сравнивайте вывод с эталонной сортировкой, а не с одной удачной трассировкой.

      При частых повторах используйте трёхчастное разбиение. На массиве из одинаковых чисел оно завершает обработку за один линейный проход. Уточните, что k-й элемент считается с кратностями: в [1, 1, 8] второй элемент равен 1, а не 8.

      ОТ РАНГА К КОНТРАКТУ

      Медиана — не всегда один элемент

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

      Что проверить перед отправкой

      • Пустой вход и недопустимый k обрабатываются контрактом.
      • Прямой и обратный порядок, все равные элементы, много повторов.
      • Индекс k корректно пересчитывается при переходе в правую часть.
      • Для t тестов оценивается сумма размеров и стоимостей, а не один средний запуск.

      Случайный pivot гарантирует, что любой конкретный запуск QuickSort завершится за O(n log n)?

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

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

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

      Продолжить: Рандомизированные алгоритмы →

      Разминка: от одного разделения к выбору ранга

      Начните с предиката «чётное»: ту же идею использует первая внешняя задача Sort Array By Parity. Затем заполните трёхчастное разбиение и напишите kth без полной сортировки. Не забывайте равные элементы и нулевую нумерацию ранга.

      Напишите partition_parity(a): верните [even,odd] — чётные и нечётные числа в исходном порядке внутри каждой группы. Не сортируйте. Это разминка к Sort Array By Parity: одного разделения достаточно, полный порядок не нужен.

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

      Открыто 0 из 3

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

        Тест 1 partition_parity([5, 2, 8, 1, 0])Ожидается: [[2, 8, 0], [5, 1]]
        Тест 2 partition_parity([])Ожидается: [[], []]

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

        Попыток: 0

        Median of Two Sorted Arrays оставлена со звёздочкой: вернитесь к ней после двоичного поиска в лекции 05.

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

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

        01

        Лёгкие

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

        1. LeetCode · 905 · Easy

          Sort Array By Parity

          Отделить полное упорядочивание от одного partition по предикату.

          Ограничения. До 5000 неотрицательных чисел, каждое не больше 5000.

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

          Сдвигайте чётные значения в префикс одним проходом. Нужны O(n) времени и O(1) дополнительной памяти для обменов. Порядок внутри чётной и нечётной групп не требуется сохранять; сортировка всех элементов избыточна.

        2. LeetCode · 169 · Easy

          Majority Element

          Использовать свойство медианы при гарантированном большинстве.

          Ограничения. До 5 · 10⁴ чисел; элемент с кратностью больше n/2 гарантирован.

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

          Такой элемент занимает центральную позицию отсортированного мультимножества. Примените выбор порядковой статистики и докажите утверждение. При отсутствии гарантии понадобилась бы проверка частоты. Линейный Boyer–Moore — отдельный альтернативный метод, не обязательный для этого разбора.

        3. Codeforces · 405A

          Gravity Flip

          Распознать задачу на порядок вместо симуляции перемещения каждого кубика.

          Ограничения. n ≤ 100, высоты столбцов от 1 до 100.

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

          Новый порядок высот неубывает. Реализуйте трёхчастный QuickSort и сравните результат с эталонной сортировкой. При малом n даже квадратичная оценка допустима по масштабу, но докажите, почему симуляция заменяется сортировкой.

        02

        Средние

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

        1. LeetCode · 215 · Medium

          Kth Largest Element in an Array

          Продолжать QuickSelect только в нужной части и правильно учитывать повторы.

          Ограничения. До 10⁵ чисел, 1 ≤ k ≤ n; значения по модулю до 10⁴.

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

          k-й наибольший соответствует нулевому индексу n−k по возрастанию. Трёхчастное разбиение позволяет сразу остановиться в блоке равных. Ожидаемое O(n) при равномерной рандомизации; медиана медиан даёт O(n) в худшем случае. Сравните с кучей размера k из прошлой лекции.

        2. LeetCode · 75 · Medium

          Sort Colors

          Поддерживать три области на месте с инвариантом голландского флага.

          Ограничения. До 300 элементов; значения только 0, 1 и 2. Готовая сортировка не разрешена.

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

          Держите зоны нулей, единиц, неизвестных и двоек. После обмена с правым краем новый элемент в текущей позиции ещё не обработан. Один проход даёт O(n) времени и O(1) памяти; предварительно выпишите инвариант всех границ.

        03

        Тяжёлая

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

        1. LeetCode · 4 · Hard

          Median of Two Sorted Arrays

          ★ После лекции 05: перенести выбор ранга на два упорядоченных массива.

          Ограничения. Каждая длина до 1000, суммарная длина положительна; требуется O(log(m+n)).

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

          Вернитесь после лекции 05 о двоичном поиске. Обычный линейный выбор здесь не выполняет явную логарифмическую цель. Используйте упорядоченность частей: исключайте блоки кандидатов или ищите согласованное разбиение. Обработайте пустую часть и среднее двух центральных значений. Задача дополнительная и не требуется для освоения QuickSelect.

        Проверьте перенос ранга и ошибку указателя.

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

        В DNF a[i] > pivot. Вы поменяли a[i] с последним неизвестным и уменьшили gt. Что делать с i?

        После разбиения |L|=2, |E|=3, глобальный k=6. Какой локальный k в правой части?

        Почему QuickSort продолжает две ветви, а QuickSelect — одну?

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

        Медиана медиан

        Необязательное углубление. Группы по пять помогают выбрать достаточно хорошую опору без случайности. Для базовых задач достаточно QuickSelect; доказательство 30% не является условием основного прогресса.

        Для гарантии нужен pivot, который при любом входе отсекает заметную долю элементов. Искать точную медиану всего массива «для выбора хорошего pivot» было бы той же исходной задачей. Вместо этого построим достаточно хороший опорный элемент из маленьких групп.

        Разобьём элементы на группы по пять и отсортируем каждую группу за постоянное время. Соберём их медианы и рекурсивно выберем медиану этого списка. Важно именно выбрать её тем же алгоритмом, а не сортировать весь список медиан.

        Схема BFPRT
        select(A, k):
            if len(A) <= 5: return sorted(A)[k]
            groups = blocksOfAtMostFive(A)
            medians = [median(group) for group in groups]
            pivot = select(medians, floor(len(medians)/2))
            L,E,R = split3(A,pivot)
            recurse into the part containing k

        Половина медиан не меньше выбранного pivot. В каждой соответствующей полной группе ещё как минимум два элемента не меньше её медианы. Получается около 3n/10 элементов не меньше pivot. Симметрично около 3n/10 элементов не больше него. Неполная группа и округления изменяют оценку только на O(1).

        Медианы пятёрок

        Группа 1

        0[0]
        2[1]
        13[2]
        15[3]
        26[4]

        Группа 2

        4[0]
        6[1]
        17[2]
        28[3]
        30[4]

        Группа 3

        8[0]
        19[1]
        21[2]
        32[3]
        34[4]

        Группа 4

        10[0]
        12[1]
        23[2]
        25[3]
        36[4]

        Группа 5

        1[0]
        3[1]
        14[2]
        16[3]
        27[4]

        Медианы групп:

        13[0]
        17[1]
        21[2]
        23[3]
        14[4]

        Медиана медиан: 17. Пятёрки отсортированы локально; весь исходный массив не сортируется.

        Один рекурсивный вызов ищет медиану среди примерно n/5 медиан, другой продолжает поиск ответа не более чем в 7n/10 + O(1) элементах. Подготовка групп и разбиение стоят O(n). Сумма долей 1/5 + 7/10 = 9/10 меньше единицы: суммарная работа на следующих уровнях убывает геометрически, что и даёт O(n).

        T(n) ≤ T(⌈n/5⌉) + T(7n/10 + O(1)) + O(n) = O(n)
        Почему именно пять?
        Группы по три не дают достаточно сильного гарантированного сокращения в этой простой рекуррентности; группы по пять позволяют сумме долей двух рекурсивных частей оставаться строго меньше 1.

        Алгоритм известен как «медиана медиан», или BFPRT. Его сильная сторона — оценка худшего случая. Практический выигрыш относительно простого QuickSelect зависит от констант и реализации; учебный код ниже прежде всего показывает, откуда берётся гарантия.

        Выбор через медиану медиан

        Индекс k начинается с нуля. Группы имеют не более пяти элементов; сортировка нужна только внутри них и в маленькой базе. Рекурсивно выбираем медиану медиан и продолжаем в одной части. Худшее время O(n), но константы заметны; эта учебная версия выделяет дополнительные массивы. Для недопустимого k возвращается None.

        Выбор через медиану медиан / Python
        def select_k(a: list[int], k: int) -> int | None:
            if not 0 <= k < len(a):
                return None
            def solve(a: list[int], k: int) -> int:
                if len(a) <= 5:
                    return sorted(a)[k]
                groups = [sorted(a[i:i+5]) for i in range(0, len(a), 5)]
                medians = [g[len(g)//2] for g in groups]
                pivot = solve(medians, len(medians)//2)
                left = [x for x in a if x < pivot]
                equal = [x for x in a if x == pivot]
                if k < len(left): return solve(left, k)
                if k < len(left) + len(equal): return pivot
                right = [x for x in a if x > pivot]
                return solve(right, k - len(left) - len(equal))
            return solve(a, k)
        
        print(select_k([9, 1, 7, 2, 8, 3, 6, 4, 5], 3))
        Ожидаемый вывод
        4

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

        Медиана без полной сортировки

        Если нужен только центральный результат среди n измерений, после выбора он доступен за O(n) в худшем случае этим методом. Для двух центральных значений при чётном n нужны два выбора или дополнительный анализ; это отличается от медианы уже отсортированных двух массивов.

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

        Откуда появляется линейная гарантия медианы медиан?

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

        Нижняя граница сортировок сравнением

        Необязательное углубление. Для трёх разных элементов есть шесть возможных порядков. Один ответ «да/нет» различает не больше двух вариантов, два ответа — не больше четырёх; иногда понадобится третий. Ниже это рассуждение обобщается на n элементов.

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

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

        2ʰ ≥ n! ⇒ h ≥ log₂(n!) = Ω(n log n)

        Почему log₂(n!) имеет порядок n log n? Сверху n! ≤ nⁿ. Снизу хотя бы n/2 множителей не меньше n/2, поэтому n! ≥ (n/2)ⁿ⁄² с поправкой на округление. Логарифмы обеих границ имеют порядок n log n.

        Сколько исходов различает дерево сравнений?
        log₂(n!)
        15.30
        Нижняя граница высоты
        16
        n log₂ n
        24.00

        Двоичное дерево высоты h имеет не более 2ʰ листьев. Чтобы различить n! порядков различных элементов, требуется h ≥ ⌈log₂(n!)⌉.

        Следовательно, хотя бы на одном входе сортировке сравнением потребуется Ω(n log n) сравнений. MergeSort и HeapSort достигают этого порядка в худшем случае. Линейный выбор статистики границу не нарушает: он отвечает на один вопрос о ранге, а не устанавливает весь порядок.

        Где ограничение не действуетCounting sort и radix sort используют свойства ключей (например, небольшой диапазон целых значений), а не только их попарный порядок. Для них нижняя граница Ω(n log n) не обязательна.

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

        К какой модели относится нижняя граница Ω(n log n)?

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

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

        Промпт для ИИ

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