К содержимому
ИС30

Поиск по сайту

Конспекты, лабы, квизы, ЧаВо и страницы

Войти
Алгоритмы и структуры данныхПрактика2 октября 2026 г.

Практика 2 октября. Сортировка подсчётом, цифровая сортировка, двоичная куча

Сортировка подсчётом с префиксными суммами и её устойчивость, цифровая сортировка LSD и MSD, разная длина ключей, время и память, приоритетная очередь, двоичная куча, просеивание вверх и вниз.

Практика из двух частей. В первой разобрали линейные сортировки: сортировку подсчётом и построенную на ней цифровую (Radix sort) в двух вариантах, LSD и MSD. Они обходят нижнюю оценку nlog⁡nn\log n, потому что вообще не сравнивают элементы. Во второй части, немного опережая лекции, началась новая тема: приоритетная очередь и двоичная куча, которая хранится в обычном массиве.

Как читать

На паре не всё было разобрано до конца: часть вопросов осталась «на подумать», а куча только началась. Такие места дописаны и отмечены словом «Дополнение». Всё остальное сказано на паре, а примеры и схемы пересчитаны вручную.

1. Лабораторная по сортировкам

В начале пары обсудили лабораторную 1 «Сортировки: музыкальный стриминг».

ЧтоПравило
Сдачаможно сдавать уже сейчас
Задачи в контесте, включая со звёздочкойзалить до дедлайна: 8 октября, 23:59 по таймеру контеста на sort-me.org
Защитаможно и после дедлайна, когда время в контесте уже истекло

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

2.1. Зачем сортировки без сравнений

На прошлой практике было доказано, что любая сортировка, основанная на сравнениях, в худшем случае не быстрее nlog⁡nn\log n (конспект). Сортировка подсчётом и цифровая сортировка работают за линейное время, потому что на эту оценку не попадают: элементы не сравниваются друг с другом, а раскладываются по своим значениям.

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

2.2. Простая версия

Пусть все значения целые и лежат в диапазоне [min⁡;max⁡][\min;\max]. Обозначим k=max⁡−min⁡+1k=\max-\min+1 — сколько разных значений вообще может встретиться.

  1. Одним проходом найти минимум и максимум.
  2. Завести массив счётчиков CC длины kk и одним проходом посчитать, сколько раз встречается каждое значение.
  3. Пройти по CC слева направо и выписать каждое значение vv столько раз, сколько оно встретилось.
counting_sort_simple.cpp
void counting_sort(vector<int>& a) {
    if (a.empty()) return;
    auto [mn, mx] = minmax_element(a.begin(), a.end());
    int lo = *mn, k = *mx - *mn + 1;
    vector<int> c(k, 0);
    for (int x : a) c[x - lo]++;          // сдвиг на lo: годится и для отрицательных
    int pos = 0;
    for (int v = 0; v < k; v++)
        while (c[v]-- > 0) a[pos++] = v + lo;
}

Эта версия годится только для «голых» чисел: она не переставляет элементы, а пишет значения заново. Если сортировать объекты по ключу (песни по году, студентов по баллу), сами объекты так не восстановить.

2.3. Версия с префиксными суммами

Для объектов CC превращают в префиксные суммы: после этого C[v]C[v] — сколько элементов имеют ключ не больше vv. Значит, последний элемент с ключом vv должен встать на позицию C[v]−1C[v]-1, предпоследний — на C[v]−2C[v]-2 и так далее.

Остаётся пройти по исходному массиву справа налево, ставить каждый элемент на позицию C[ключ]−1C[\text{ключ}]-1 в новый массив BB и уменьшать счётчик.

Сортировка подсчётом: счётчики, префиксные суммы и заполнение выходного массива справа налево

counting_sort.cpp
void counting_sort(vector<int>& a) {
    if (a.empty()) return;
    auto [mn, mx] = minmax_element(a.begin(), a.end());
    int lo = *mn, k = *mx - *mn + 1;
    vector<int> c(k, 0);
    for (int x : a) c[x - lo]++;
    for (int v = 1; v < k; v++) c[v] += c[v - 1];  // c[v] = сколько элементов <= v
    vector<int> b(a.size());
    for (int i = (int)a.size() - 1; i >= 0; i--)   // справа налево
        b[--c[a[i] - lo]] = a[i];
    a = move(b);
}

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

Для одной сортировки по одному ключу это может быть неважно. Для цифровой сортировки, которая строится из нескольких проходов подсчётом, устойчивость обязательна (§3.2).

2.4. Время и память

По времени алгоритм — это несколько линейных проходов подряд:

ШагВремя
найти минимум и максимумΘ(n)\Theta(n)
посчитать счётчикиΘ(n)\Theta(n)
префиксные суммы по CCΘ(k)\Theta(k)
разложить элементы в BBΘ(n)\Theta(n)
итогоΘ(n+k)\Theta(n+k)

По памяти — массив CC длины kk и, в версии с префиксными суммами, выходной массив BB длины nn: Θ(n+k)\Theta(n+k). В простой версии только Θ(k)\Theta(k).

Ни n, ни k не выбрасывать

Частая ошибка — считать kk константой и писать Θ(n)\Theta(n). Заранее неизвестно, что больше: kk может быть намного больше nn, а может быть намного меньше. Поэтому в оценке остаются обе буквы.

Отсюда и главное ограничение: подсчёт хорош, только когда диапазон значений небольшой. Для массива из двух чисел 10610^6 и 11 придётся завести миллион счётчиков ради двух элементов. При огромном диапазоне массив CC может вообще не поместиться в память.

3. Цифровая сортировка

3.1. Идея

Цифровая (поразрядная) сортировка, она же Radix sort, сортирует числа или строки, «если они не супер большие». Ключ разбивается на разряды: цифры числа или символы строки. У каждого разряда диапазон маленький (10 цифр, 26 латинских букв), поэтому по одному разряду можно сортировать подсчётом.

Обозначения, которые будут дальше:

БукваСмысл
nnколичество ключей
ddколичество разрядов в ключе
kkдиапазон одного разряда: сколько разных цифр или символов

Разряды можно перебирать с конца (LSD, least significant digit — младший разряд) или с начала (MSD, most significant digit — старший разряд). Для понимания удобнее начинать с конца.

3.2. LSD: от последнего разряда к первому

Сначала массив сортируется по последнему символу, потом по предпоследнему и так до первого. Каждый проход — устойчивая сортировка подсчётом по одному символу.

LSD: три устойчивых прохода от последнего символа к первому

Зачем устойчивость. Посмотрите на aba и abc на последнем проходе. Обе начинаются с a, и по первому символу они равны. Правильный порядок между ними уже установлен предыдущими проходами: по второму символу они тоже равны, а по третьему a меньше c. Устойчивая сортировка оставит их в этом порядке. Неустойчивая может их переставить: результат прошлых проходов потеряется, и вся сортировка сломается.

Отсюда инвариант LSD: после jj проходов массив отсортирован по последним jj символам. Очередной проход упорядочивает по новому символу, а при равенстве сохраняет уже готовый порядок по младшим.

3.3. Ключи разной длины

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

  • Числа дополняются нулями слева: 7→0077\to007. Ведущие нули значение не меняют.
  • Строки дополняются справа символом, которого нет в алфавите и который меньше всех его символов. Например, $: его код в ASCII 36, а у букв — от 65 (A) и от 97 (a). Тогда ab превратится в ab$ и окажется раньше abc, как и положено в лексикографическом порядке: префикс идёт раньше более длинной строки.

Дополнять строки самой маленькой буквой алфавита, например a, нельзя: ab превратится в aba, и строки ab и aba станут неотличимы. Алгоритм не будет знать, какая из них короче.

3.4. Время LSD

Делается dd проходов, каждый — сортировка подсчётом за Θ(n+k)\Theta(n+k): T(n)=Θ(d (n+k)).T(n)=\Theta\big(d\,(n+k)\big).

Считать kk константой можно, только если алфавит заранее ограничен. В примере выше были только маленькие латинские буквы, k=26k=26. Но в сервисе перевода или локализации строки могут содержать символы практически любых алфавитов, а также собственные символы. Тогда kk огромно, и выбросить его из оценки нельзя.

Память LSD — Θ(n+k)\Theta(n+k): один массив счётчиков и один буфер, которые переиспользуются на каждом проходе.

Дополнение: числа по байтам

На паре спросили, можно ли вместо десятичных цифр брать, например, по 8 бит. Можно, и на практике так и делают. 32-битное число — это 4 байта, то есть d=4d=4, k=28=256k=2^8=256. Время 4(n+256)4(n+256). По числу шагов это заметно меньше, чем nlog⁡2nn\log_2n сравнений: при n=106n=10^6 логарифм около 20.

radix_sort_lsd.cpp
void radix_sort(vector<uint32_t>& a) {
    vector<uint32_t> b(a.size());
    for (int shift = 0; shift < 32; shift += 8) {   // d = 4 прохода, от младшего байта
        array<size_t, 256> c{};                      // k = 256
        for (uint32_t x : a) c[(x >> shift) & 255]++;
        for (int v = 1; v < 256; v++) c[v] += c[v - 1];
        for (size_t i = a.size(); i-- > 0;)         // справа налево — устойчиво
            b[--c[(a[i] >> shift) & 255]] = a[i];
        swap(a, b);                                  // проходов чётное число, ответ в a
    }
}

3.5. MSD: от первого разряда к последнему

Если просто отсортировать сначала по первому символу, потом по второму, потом по третьему, получится ерунда. Каждый следующий проход перемешает то, что упорядочил предыдущий. Поэтому MSD устроен иначе.

  1. Сортируем подсчётом по первому символу.
  2. Строки с одинаковым первым символом теперь стоят подряд — это блоки («корзины»). Строки из разных корзин больше никогда не перемешиваются: всё, что начинается на a, меньше всего, что начинается на b.
  3. Рекурсивно запускаемся от каждой корзины, сортируя её уже по второму символу, и так далее.
  4. Рекурсия останавливается, когда в корзине остался один элемент или символы кончились.

MSD: разбиение на корзины по первому символу и рекурсия внутри корзин

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

Глубина рекурсии — до dd, а не log⁡d\log d. Каждый уровень рекурсии «съедает» ровно один символ, поэтому до dd-го символа можно дойти только через dd уровней.

radix_sort_msd.cpp
// a[l..r) сортируются по символам с позиции pos;
// все строки заранее дополнены '$' до одной длины
void msd(vector<string>& a, int l, int r, int pos) {
    if (r - l <= 1 || pos == (int)a[l].size()) return;
    array<int, 257> c{};                                   // k = 256
    for (int i = l; i < r; i++) c[(unsigned char)a[i][pos] + 1]++;
    for (int v = 1; v <= 256; v++) c[v] += c[v - 1];      // c[v] = начало корзины v
    {
        vector<string> b(r - l);
        for (int i = l; i < r; i++) b[c[(unsigned char)a[i][pos]]++] = move(a[i]);
        for (int i = l; i < r; i++) a[i] = move(b[i - l]);
    }                                                      // буфер освобождён до рекурсии
    for (int v = 0, start = 0; v < 256; start = c[v++])   // теперь c[v] = конец корзины v
        msd(a, l + start, l + c[v], pos + 1);
}

3.6. LSD или MSD

Какой вариант быстрее, на паре обсуждали как спор двух команд и до конца не решили.

За MSD. LSD всегда проходит по всем nn ключам на каждом из dd разрядов. MSD останавливается, как только корзина стала маленькой. Если ключи различаются уже в первых символах, до остальных символов он вообще не доходит. На схеме выше до третьего символа дошли только aba и abc.

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

За LSD (дополнение, на паре этот тезис доказать не успели). LSD проще: нет рекурсии, память Θ(n+k)\Theta(n+k) при любых данных, каждый проход — один линейный пробег по массиву. MSD платит Θ(k)\Theta(k) за каждую корзину, и на множестве мелких корзин это обходится дорого (см. ниже).

Память. У MSD к сортировке подсчётом добавляется рекурсия. На каждом уровне рекурсии лежит свой массив счётчиков длины kk. Значение kk одно на весь алфавит и в глубине не меняется, а nn на память счётчиков не влияет.

LSDMSD
Порядок разрядовс последнегос первого
Устойчивость каждого проходаобязательнадля правильности не нужна: корзины дальше сортируются сами
ВремяΘ(d(n+k))\Theta\big(d(n+k)\big) всегдаO(d⋅n)O(d\cdot n) на элементы плюс Θ(k)\Theta(k) на каждую корзину
Может закончить раньшенет, всегда dd проходовда, на корзинах из одного элемента
Дополнительная памятьΘ(n+k)\Theta(n+k)лучший случай — глубина 1; худший — глубина dd, O(n+d⋅k)O(n+d\cdot k)
Дополнение: слабое место MSD

Каждый вызов MSD тратит Θ(k)\Theta(k) на массив счётчиков, даже если в корзине всего два элемента. Худший случай — строки, которые идут парами с длинным общим началом: на каждом уровне около n/2n/2 корзин по два элемента, и время вырастает до Θ(d⋅n⋅k)\Theta(d\cdot n\cdot k). Поэтому на практике маленькие корзины (до нескольких десятков элементов) досортировывают вставками.

4. Приоритетная очередь и двоичная куча

4.1. Задача

Есть объекты, у каждого — приоритет. Объекты добавляются и удаляются, приоритеты могут меняться. Важно в любой момент знать, кто следующий: кто первым зайдёт к врачу, кто первым сядет в самолёт. Человек с высшим приоритетом идёт первым, остальные «когда-то потом».

Если хранить объекты просто в массиве, следующего каждый раз приходится искать заново. Нужна структура, которая сама поддерживает инвариант «наверху — самый приоритетный элемент» и дёшево его восстанавливает после изменений.

Дополнение. Сравнение с наивными способами:

СтруктураУзнать минимумДобавитьИзвлечь минимум
неотсортированный массивO(n)O(n)O(1)O(1)O(n)O(n)
отсортированный массивO(1)O(1)O(n)O(n)O(1)O(1), если минимум хранить в конце
двоичная кучаO(1)O(1)O(log⁡n)O(\log n)O(log⁡n)O(\log n)

4.2. Куча

Определение 1. Min-куча (min-heap) — дерево, в котором каждый узел не больше своих детей. Max-куча (max-heap) — наоборот, каждый узел не меньше своих детей. По-русски кучу ещё называют пирамидой.

Дальше везде min-куча: наверху минимальный элемент. Его и считаем элементом с наивысшим приоритетом. Max-куча устроена так же с обратным знаком сравнения.

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

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

4.3. Полное двоичное дерево

Двоичная (бинарная) куча строится на полном двоичном дереве:

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

Требования «у каждого узла 0 или 2 ребёнка» тут нет: у последнего заполненного узла может быть один левый ребёнок, а узлы последнего уровня — листья.

Дополнение: путаница в терминах

В англоязычной литературе это complete binary tree. Full binary tree — другое понятие: у каждого узла 0 или 2 ребёнка, именно это и спросили на паре. Perfect — все уровни заполнены целиком. Русское «полное» в разных книгах переводит то одно, то другое, поэтому на коллоквиуме лучше сразу сказать определение: «заполнено по уровням слева направо».

Высота полного двоичного дерева из nn узлов — ⌊log⁡2n⌋\lfloor\log_2n\rfloor. Уровень ℓ\ell, если он заполнен целиком, содержит 2ℓ2^\ell узлов. Поэтому дерево высоты hh содержит от 2h2^h до 2h+1−12^{h+1}-1 узлов, откуда h=⌊log⁡2n⌋h=\lfloor\log_2n\rfloor. Отсюда все логарифмы ниже.

4.4. Хранение в массиве

Раз дерево заполняется строго по уровням и слева направо, указатели не нужны: узлы можно выписать в массив в том же порядке. Корень — на индексе 00, потом весь уровень 11, потом весь уровень 22 и так далее.

Двоичная куча как дерево и как массив: дети узла i на индексах 2i+1 и 2i+2, родитель на (i−1)/2 с округлением вниз

Для узла с индексом ii (индексы с нуля):

КтоИндекс
левый ребёнок2i+12i+1
правый ребёнок2i+22i+2
родитель⌊i−12⌋\left\lfloor\dfrac{i-1}2\right\rfloor

Формула родителя — обратная операция к формулам детей. Округление вниз: левый ребёнок 2i+12i+1 нечётный, правый 2i+22i+2 чётный, и для обоих должно получиться одно и то же ii. Проверка на паре: у узла 99 родитель 9−12=4\frac{9-1}2=4, у узла 1010 — ⌊10−12⌋=⌊4,5⌋=4\left\lfloor\frac{10-1}2\right\rfloor=\lfloor4{,}5\rfloor=4. Оба — дети узла 44: 2⋅4+1=92\cdot4+1=9, 2⋅4+2=102\cdot4+2=10. Детей узла 22 ищем так же: 2⋅2+1=52\cdot2+1=5 и 2⋅2+2=62\cdot2+2=6.

В C++ деление неотрицательных целых (i - 1) / 2 само округляет вниз. Но для корня (0−1)/2(0-1)/2 даёт 00, а не −1-1, потому что деление округляет к нулю. Поэтому цикл подъёма должен явно останавливаться на i=0i=0.

Дополнение: откуда формулы. Уровень ℓ\ell начинается с индекса 2ℓ−12^\ell-1. Если ii — pp-й узел своего уровня (i=2ℓ−1+pi=2^\ell-1+p), то перед его детьми на следующем уровне стоят 2p2p узлов. Значит, левый ребёнок — 2ℓ+1−1+2p=2(2ℓ−1+p)+1=2i+12^{\ell+1}-1+2p=2(2^\ell-1+p)+1=2i+1.

4.5. Вставка: просеивание вверх

Новый элемент кладётся в конец массива, то есть в первую свободную позицию последнего уровня. Если он не меньше родителя, всё в порядке. Если меньше, свойство кучи нарушено, и элемент нужно поднять. Эта операция называется sift up (просеивание вверх):

  1. сравнить элемент с родителем;
  2. если элемент меньше — поменять их местами и повторить с новой позиции;
  3. остановиться, когда элемент не меньше родителя или стал корнем.

Вставка 3 в кучу: 3 меняется с 5, затем с 4 и останавливается под корнем 1

Почему обмен ничего не ломает: родитель был не больше своего второго ребёнка, а новый элемент меньше родителя, значит, меньше и этого ребёнка. Если вставить 00, он поднимется до самого корня.

Время — O(log⁡n)O(\log n): за каждый обмен элемент поднимается на уровень, а уровней ⌊log⁡2n⌋+1\lfloor\log_2n\rfloor+1.

На паре спросили, нельзя ли быстрее, за log⁡log⁡n\log\log n. В такой простой структуре нельзя. Бывают другие, более сложные кучи с другими оценками.

4.6. Извлечение минимума: просеивание вниз

Минимум лежит в корне — посмотреть его можно за O(1)O(1). Чтобы его удалить:

  1. поменять корень с последним элементом массива — в массиве это делается легко;
  2. уменьшить размер на один (resize или просто сдвинуть правую границу) — бывший минимум больше не участвует;
  3. на месте корня теперь чужой, скорее всего большой элемент. Его опускают операцией sift down (просеивание вниз): сравнить с обоими детьми и поменять с меньшим из них;
  4. остановиться, когда детей нет или элемент не больше обоих детей.

Извлечение минимума: корень 1 меняется с последним элементом 5, затем 5 опускается на место меньших детей 3 и 4

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

Время — O(log⁡n)O(\log n): на каждом шаге элемент спускается на уровень, так что шагов не больше высоты дерева. Удаление — это обмен за O(1)O(1) плюс sift down, итого тоже O(log⁡n)O(\log n).

4.7. Код

Дополнение — та же логика на C++:

min_heap.cpp
struct MinHeap {
    vector<int> a;
 
    void sift_up(int i) {
        while (i > 0 && a[i] < a[(i - 1) / 2]) {
            swap(a[i], a[(i - 1) / 2]);
            i = (i - 1) / 2;
        }
    }
 
    void sift_down(int i) {
        int n = a.size();
        while (true) {
            int l = 2 * i + 1, r = 2 * i + 2, m = i;
            if (l < n && a[l] < a[m]) m = l;   // m — наименьший из трёх
            if (r < n && a[r] < a[m]) m = r;
            if (m == i) return;                // не больше детей или детей нет
            swap(a[i], a[m]);
            i = m;
        }
    }
 
    int top() const { return a[0]; }                       // O(1)
    void push(int x) { a.push_back(x); sift_up(a.size() - 1); }  // O(log n)
    void pop() {                                           // O(log n)
        swap(a[0], a.back());
        a.pop_back();
        if (!a.empty()) sift_down(0);
    }
};

В стандартной библиотеке это std::priority_queue. По умолчанию она max-куча: top() возвращает наибольший элемент. Min-куча объявляется как priority_queue<int, vector<int>, greater<int>>.

4.8. Что дальше

ОперацияКакВремя
узнать минимумa[0]O(1)O(1)
вставкав конец + sift upO(log⁡n)O(\log n)
извлечь минимумобмен с последним + sift downO(log⁡n)O(\log n)

Если разобраться с sift up и sift down, все остальные операции кучи получаются из них автоматически. На паре остался открытый вопрос: за сколько построить кучу из готового массива. Его разберут на лекции.

Дополнение: построение кучи

Наивно — nn вставок, O(nlog⁡n)O(n\log n). Быстрее так: вызвать sift down для всех узлов, у которых есть дети, от последнего такого узла (индекс ⌊n/2⌋−1\lfloor n/2\rfloor-1) к корню. Большинство узлов лежат внизу и спускаются всего на 1–2 уровня: на высоте hh около n/2h+1n/2^{h+1} узлов. Сумма ∑hh⋅n2h+1\sum_h h\cdot\frac n{2^{h+1}} ограничена nn, поэтому построение работает за O(n)O(n). На этом же построена пирамидальная сортировка (heap sort): построить max-кучу и nn раз извлечь максимум в конец массива.

Частые ошибки

  • «Сортировка подсчётом работает за Θ(n)\Theta(n)». Нет, за Θ(n+k)\Theta(n+k). При большом диапазоне kk доминирует.
  • Префиксные суммы, а потом проход слева направо. Сортировка всё равно получится, но равные элементы выйдут в обратном порядке. Для LSD это ошибка.
  • LSD с неустойчивой сортировкой по разряду. Каждый проход ломает результат предыдущих.
  • MSD как «LSD наоборот» — просто несколько полных проходов с первого символа. Без разбиения на корзины и рекурсии ответ неверный.
  • Дополнять строки самой маленькой буквой, а не символом вне алфавита. ab и aba становятся неотличимы.
  • «Полное двоичное дерево — у каждого узла 0 или 2 ребёнка». У кучи «полное» значит «заполнено по уровням слева направо».
  • Sift down с любым ребёнком. Менять нужно только с меньшим (в max-куче — с большим).
  • Родитель i2\frac i2 вместо ⌊i−12⌋\left\lfloor\frac{i-1}2\right\rfloor при индексации с нуля. Формула i2\frac i2 верна, только когда корень на индексе 11.

Мини-тренажёр

  1. Отсортируйте подсчётом массив 2,0,2,1,02,0,2,1,0. Выпишите kk, массив счётчиков и префиксные суммы.
  2. Сортировка LSD по десятичным цифрам: каким станет массив 170,45,75,90,802,24,2,66170,45,75,90,802,24,2,66 после первого прохода?
  3. Сколько проходов сделает LSD по байтам для 64-битных чисел и чему равно kk?
  4. Куча хранится в массиве 2,6,3,9,7,82,6,3,9,7,8. Назовите индекс родителя элемента с индексом 55 и индексы детей элемента с индексом 11.
  5. Вставьте 11 в кучу из задачи 4. Каким станет массив?
  6. Извлеките минимум из кучи 2,6,3,9,7,82,6,3,9,7,8 (исходной). Каким станет массив?
  7. Почему у MSD глубина рекурсии dd, а не log⁡d\log d?
Ответы
  1. min⁡=0\min=0, max⁡=2\max=2, k=3k=3. Счётчики C=(2,1,2)C=(2,1,2), префиксные суммы (2,3,5)(2,3,5). Ответ: 0,0,1,2,20,0,1,2,2.
  2. Сортировка по единицам, устойчиво: 170,90,802,2,24,45,75,66170,90,802,2,24,45,75,66. Числа 170170 и 9090 с цифрой 00, а также 802802 и 22 с цифрой 22 сохранили исходный порядок.
  3. d=8d=8 байт, k=256k=256. Время 8(n+256)8(n+256).
  4. Родитель: ⌊5−12⌋=2\left\lfloor\frac{5-1}2\right\rfloor=2 (там 33). Дети: 2⋅1+1=32\cdot1+1=3 и 2⋅1+2=42\cdot1+2=4 (там 99 и 77).
  5. 11 встаёт на индекс 66, его родитель — индекс 22 (33), меняем: 2,6,1,9,7,8,32,6,1,9,7,8,3. Родитель индекса 22 — корень (22), меняем: 1,6,2,9,7,8,31,6,2,9,7,8,3.
  6. Меняем корень с последним и отрезаем 22: 8,6,3,9,78,6,3,9,7. Дети 88 — это 66 и 33, меняем с меньшим: 3,6,8,9,73,6,8,9,7. У индекса 22 детей нет (2⋅2+1=5≥52\cdot2+1=5\ge5). Ответ: 3,6,8,9,73,6,8,9,7, минимум — 22.
  7. Каждый уровень рекурсии обрабатывает ровно один следующий символ. Чтобы дойти до dd-го символа, нужно dd вложенных вызовов, и делить задачу «пополам по символам» не получается.

Шпаргалка

ПонятиеСуть
Нижняя оценка nlog⁡nn\log nтолько для сортировок сравнениями; подсчёт и Radix sort её обходят
Сортировка подсчётомсчётчики значений, префиксные суммы, проход справа налево; Θ(n+k)\Theta(n+k) по времени и памяти
kk в подсчётеmax⁡−min⁡+1\max-\min+1; не константа, из оценки не выбрасывается
Устойчивостьравные элементы сохраняют исходный порядок; у подсчёта — за счёт прохода справа налево
LSDразряды с последнего, каждый проход устойчивый; Θ(d(n+k))\Theta(d(n+k)), память Θ(n+k)\Theta(n+k)
MSDразряды с первого, корзины и рекурсия, как в быстрой сортировке; останавливается на корзинах из одного элемента
Разная длиначисла — нули слева; строки — символ меньше алфавита ($) справа
Приоритетная очередьбыстро узнать и извлечь элемент с наивысшим приоритетом при добавлениях и удалениях
Min-кучакаждый узел не больше детей, минимум в корне; любое поддерево — тоже куча
Полное двоичное дереводо двух детей, заполняется по уровням слева направо; высота ⌊log⁡2n⌋\lfloor\log_2n\rfloor
Куча в массиведети 2i+12i+1, 2i+22i+2, родитель ⌊(i−1)/2⌋\lfloor(i-1)/2\rfloor
Sift upвставка в конец, обмен с родителем, пока меньше; O(log⁡n)O(\log n)
Sift downкорень меняют с последним, опускают на место меньшего ребёнка; O(log⁡n)O(\log n)

Проверь себя

24 вопроса по материалу лекции. Результаты хранятся только в вашем браузере.

24 вопросов о сортировке подсчётом и её устойчивости, LSD и MSD, выравнивании ключей, двоичной куче, её хранении в массиве и просеивании.

  • 24 вопроса
  • Результат виден сразу после каждого ответа
  • Порядок вопросов случайный

Комментарии0

Пока никто ничего не написал.

Войдите, чтобы оставить комментарий