class Algorithms {
public:
Algorithms lecture {
.author = "Р.А. Нестеров"
.year = "2026/27"
.course = "НИУ ВШЭ ПИ, 2 курс"
};
// 01. Алгоритм Лас-Вегас со случайной сложностью
void quick_sort(Iter first, Iter last);
void expected_comparisons();
// 02. ADT Очередь с приоритетами и пирамидальная сортировка
template<typename T> class PriorityQueue {...};
void heap_sort(std::vector& vec);
// 03. Сортировки на сравнениях [cb-сортировки] и граница оптимальности
void build_decision_tree();
void comparisons_lower_bound();
->06
06.00
метод Монте-Карло: проба пространства
проверка корректности матричного умножения
за $\mathrm{O}(n^2)$ c приемлемой вероятностью ошибки
на проверку поступили три матрицы:
$A = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} B = \begin{pmatrix} 5 & 6 \\ 7 & 8 \end{pmatrix} C = \begin{pmatrix} \textcolor{#FB7171}{18} & 22 \\ 43 & 50 \end{pmatrix}$
определим матрицу ошибок $D$:
$D = AB - C = \begin{pmatrix} \textcolor{#FB7171}{\Delta} & 0 \\ 0 & 0 \end{pmatrix}$
умножаем матрицу ошибок $D$ на случайный бинарный вектор-детектор $r = (r_1 \,\, r_2)^T$, то есть, $r \in \{0, 1\}^2$:
$Dr = \begin{pmatrix} \textcolor{#FB7171}{\Delta} & 0 \\ 0 & 0 \end{pmatrix} \cdot \begin{pmatrix} r_1 \\ r_2 \end{pmatrix} = \begin{pmatrix} \textcolor{#FB7171}{\Delta}\cdot r_1 \\ 0 \end{pmatrix}$
06.01
схема быстрой сортировки
выбор опорного элемента и его влияние
на сложность сортировки
экспериментальная оценка
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
template<typename Iter>
void merge_sort(Iter first, Iter last) {
if (std::distance(first, last) <= 1) return;
auto mid = first + std::distance(first, last) / 2;
merge_sort(first, mid);
merge_sort(mid, last);
merge(first, mid, last);
}
int main() {
std::vector<int> arr = {5, 2, 4, 6, 1, 3};
merge_sort(arr.begin(), arr.end());
std::cout << std::boolalpha;
std::cout << std::is_sorted(arr.begin(), arr.end());
}
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
template<typename Iter>
void quick_sort(Iter first, Iter last) {
if (std::distance(first, last) <= 1) return;
auto pivot_pos = partition(first, last);
quick_sort(first, pivot_pos);
quick_sort(std::next(pivot_pos), last);
}
int main() {
std::vector<int> arr = {5, 2, 4, 6, 1, 3};
quick_sort(arr.begin(), arr.end());
std::cout << std::boolalpha;
std::cout << std::is_sorted(arr.begin(), arr.end());
}
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
template<typename Iter>
void quick_sort(Iter first, Iter last) {
if (std::distance(first, last) <= 1) return;
auto pivot_pos = partition(first, last);
quick_sort(first, pivot_pos);
quick_sort(std::next(pivot_pos), last);
}
int main() {
std::vector<int> arr = {5, 2, 4, 6, 1, 3};
quick_sort(arr.begin(), arr.end());
std::cout << std::boolalpha;
std::cout << std::is_sorted(arr.begin(), arr.end());
}
функция partition выбирает опорный элемент и распределяет элементы массива относительно него
общее соотношение: $T(n) = T(n-c) + T(c) + kn$
void expected_comparisons();
оценка математического ожидания числа сравнений
$\mathbb{E}[X] \leqslant 2n \ln n \approx 1.39 n \log_2 n$
введем индикаторную случайную величину $X_{ij}$ для каждой пары элементов $s_i$ и $s_j$, где $i < j$: $ X_{ij} = \begin{cases} 1, & \text{если } s_i \text{ и } s_j \text{ сравнивались} \\ 0, & \text{в противном случае} \end{cases} $
общее количество сравнений $X$ — сумма индикаторов: $ X = \displaystyle\sum_{i=1}^{n-1} \sum_{j=i+1}^{n} X_{ij} $
void expected_comparisons();
оценка математического ожидания числа сравнений
$\mathbb{E}[X] \leqslant 2n \ln n \approx 1.39 n \log_2 n$
выразим математическое ожидание $X$: $\displaystyle \mathbb{E}[X] = \mathbb{E}\left[ \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} X_{ij} \right] = \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} \mathbb{E}[X_{ij}]$
величина $X_{ij}$ распределена по закону Бернулли, поэтому: $\displaystyle \mathbb{E}[X_{ij}] = 1 \cdot P(X_{ij} = 1) + 0 \cdot P(X_{ij} = 0) = P(s_i \leftrightarrow s_j)$
таким образом, целевое уравнение для $\mathbb{E}[X]$: $\displaystyle \mathbb{E}[X] = \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} P(s_i \leftrightarrow s_j)$
void expected_comparisons();
оценка математического ожидания числа сравнений
$\mathbb{E}[X] \leqslant 2n \ln n \approx 1.39 n \log_2 n$
рассмотрим множество $S_{ij}$ элементов между $s_i$ и $s_j$: $\displaystyle S_{ij} = \{s_i, s_{i+1}, \dots, s_j\}$ — отрезок упорядоченных элементов $A$
необходимо определить, когда элементы $s_i$ и $s_j$ сравниваются в разных ситуациях выбора опорного элемента:
вероятность выбрать именно $s_i$ или $s_j$: $\displaystyle P(s_i \leftrightarrow s_j) = \frac{2}{j - i + 1}$, поскольку выбор равновероятен
void expected_comparisons();
оценка математического ожидания числа сравнений
$\mathbb{E}[X] \leqslant 2n \ln n \approx 1.39 n \log_2 n$
подставим найденную вероятность в сумму для $\mathbb{E}(X)$: $\displaystyle \mathbb{E}[X] = \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} P(s_i \leftrightarrow s_j) = \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} \frac{2}{j - i + 1}$
выполним замену $k = j - i + 1$ — когда $j$ идет от $i+1$ до $n$, переменная $k$ будет изменяться от $2$ до $n - i + 1$: $\displaystyle \mathbb{E}[X] = \sum_{i=1}^{n-1} \sum_{k=2}^{n-i+1} \frac{2}{k}$
для упрощения оценки расширим границы суммирования: $\displaystyle \mathbb{E}[X] = \sum_{i=1}^{n-1} \sum_{k=2}^{n-i+1} \frac{2}{k} \le \sum_{i=1}^{n} \sum_{k=1}^n \frac{2}{k} = 2 \sum_{i=1}^{n} \sum_{k=1}^n \frac{1}{k}$
void expected_comparisons();
оценка математического ожидания числа сравнений
$\mathbb{E}[X] \leqslant 2n \ln n \approx 1.39 n \log_2 n$
вспомним сумму гармонического ряда: $\displaystyle H_n = \sum_{k=1}^{n} \frac{1}{k} = \ln n + \gamma + O\left(\frac{1}{n}\right)$
подставим $H_n$ вместо внутренней суммы в $E(X)$: $\displaystyle E[X] \le 2 \sum_{i=1}^{n} \sum_{k=1}^{n} \frac{1}{k} = 2 \sum_{i=1}^{n} H_n = 2n \cdot H_n$
таким образом, с учетом перехода к двоичному логарифму:
$\begin{aligned} \mathbb{E}[X] \leqslant 2n (\ln n + \Theta(1)) & = 2n \ln n + \Theta(n) \end{aligned}$
$\begin{aligned} \phantom{\mathbb{E}[X] \leqslant 2n (\ln n + \Theta(1))} & = 2n \frac{\log_2 n}{1.4426} + \Theta(n) \end{aligned}$
$\begin{aligned} \phantom{\mathbb{E}[X] \leqslant 2n (\ln n + \Theta(1)) } & \approx 1.39 n \log_2 n \end{aligned}$
| Алгоритм | 1 000 | 5 000 | 10 000 | 50 000 | 100 000 | Комментарий |
|---|---|---|---|---|---|---|
| std::sort BEST | 5.4 | 29.9 | 65.7 | 565.3 | 1 273.1 | introsort (гибридный алгоритм) |
| qs_median GOOD | 7.3 | 46.0 | 199.6 | 1 719.7 | 3 589.6 | медиана трёх: баланс скорости и защиты от $\mathrm{O}(n^2)$ |
| qs_last OK | 8.7 | 71.7 | 184.1 | 1 742.6 | 3 746.8 | последний элемент как pivot |
| qs_first OK | 10.6 | 99.3 | 255.0 | 1 820.8 | 3 895.3 | первый элемент как pivot |
| qs_random SLOW | 15.8 | 98.9 | 297.3 | 2 150.2 | 4 796.5 | случайный pivot: накладные расходы |
| merge_sort SLOWEST | 35.6 | 214.6 | 449.3 | 2 485.1 | 5 186.2 | $\mathrm{O}(n)$ памяти и копирование в буфер при слиянии |
*время в наносекундах, необходимое для сортировки случайных массивов, различных размеров [результаты статистически стабильны]
06.02
внутренняя реализация интерфейса очереди с приоритетами: одномерный массив
двухэтапная сортировка HEAP SORT на основе
очереди с приоритетами
template<typename Key>
class PriorityQueue {
public:
// вставка с учетом приоритета
void push(Key val);
// доступ и удаление элемента с
// самым высоким приоритетом
Key pop();
Key top();
// изменение приоритета
void change_priority(Key old, Key new);
private:
// внутренний контейнер
}
Key PriorityQueue<Key>::top() {
// поиск самого приоритетного значения
for (int pri = 0; pri < M; ++pri) {
if (!queues_[pri].empty()) {
return queues_[pri].front();
}
}
}
выбор индексированного хранения очередей непрактичен при заранее неизвестном количестве приоритетов
template<typename Key> class PriorityQueue { ... }
основной способ реализации интерфейса очереди с приоритетами
int parent(int i) { return i / 2; }
int left(int i) { return 2 * i; }
int right(int i) { return 2 * i + 1; }
// восстановление инварианта кучи снизу вверх
void swim_up(int k) {
while ((k > 1) && (heap_[k / 2] < heap_[k])) {
std::swap(heap_[k / 2], heap_[k]);
k = k / 2;
}
}
// восстановление инварианта кучи сверху вниз
void sink_down(int k) {
while (2 * k <= n) {
int j = 2 * k;
if ((j < n) && (heap_[j] < heap[j + 1])) ++j;
std::swap(heap_[k], heap[j]);
k = j;
}
}
#include <algorithm>
#include <vector>
#include <iostream>
void heap_sort(std::vector<int>& arr) {
std::make_heap(arr.begin(), arr.end());
std::sort_heap(arr.begin(), arr.end());
}
int main() {
std::vector<int> vec{8, 3, 1, 7, 4, 2, 6, 5};
heap_sort(vec.begin(), vec.end());
std::cout << std::is_sorted(vec.begin(), vec.end());
}
06.03
общая модель сортировки: дерево решений
нижняя граница сравнений: критерий оптимальности сортировок
void build_decision_tree();
общая модель вычислений в сортировке на сравнениях [ cb-сортировке ]
void comparisons_lower_bound();
явная оценка высоты дерева решений cb-сортировки
ответ $h = \Omega(n \log n)$ при $c = 0.5$ и достаточно больших $n$
void comparisons_lower_bound();
явная оценка высоты дерева решений cb-сортировки
06.04
задачи и вопросы
ограничения и возможности
19 октября в 13:00 и в 14:40 — лекционое время
асимптотический анализ, рекурсия, разделяй-и-властвуй, линейные контейнеры, алгоритмы сортировки, ...
// Лекция 06 завершена
// Резюме основных моментов
быстрая сортировка
и вероятностный анализ ее сложности
реализация интерфейса типа данных и
пирамидальная сортировка
дерево решений и утверждение о
нижней границе количества сравнений
--> на следующей лекции
--> специализированные линейные сортировки
};
return 0;
06->