О чём лекция
Две темы. Первая — решето Эратосфена: все простые числа до N сразу, за вместо проверки каждого числа по отдельности. Вторая — сортировка: что это такое, по каким свойствам сравнивают сортировки (время, память, устойчивость) и три простейших алгоритма — выбором, вставками и пузырьком. Все три работают за ; в следующей лекции это станет поводом искать быстрее.
Как читать
Доказательства в блоках «Не из лекции» дописаны к конспекту: на лекции их не было. Всё остальное — с доски.
Маршрут
- Все простые до N → Решето Эратосфена
- Что такое сортировка и чем сортировки отличаются → Сортировка
- Выбором, вставками, пузырьком → Квадратичные сортировки
- Как записывать оценки → Асимптотические оценки, Время работы алгоритма
Запись
a ... bв псевдокодеНа досках не уточняется, входит ли правая граница. Смотреть надо по смыслу. В решете
for d = 2 ... Nграница входит: массив на элемент, и число N тоже надо проверить. В сортировкахfor i = 0 ... Nграница не входит — это полуинтервал , индексы массива из N элементов. Иначе пузырёк с егоarr[j+1]приj = 0 ... N−i−1вышел бы за массив уже на первом проходе.
1. Решето Эратосфена
Задача. N — натуральное число. Найти все простые .
Простое: , и делится только на и на 1 — и всё.
Идея алгоритма
Выписываем числа от 0 до N и отмечаем: ✓ — простое, ✗ — нет.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 ... N
✗ ✗ ✓ ✓ ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✓ ✗ ✓ ...- 0 и 1 — не простые.
- Идём по числам слева направо:
- не простое (✗) — пропускаем;
- простое (✓) — вычёркиваем , где перебираем.
Алгоритм
is_prime = [T, T, ..., T] // N + 1 элемент
is_prime[0] = is_prime[1] = F
for d = 2 ... N
если is_prime[d] = T // d — простое
m = 2
while (m * d ≤ N)
is_prime[m * d] = F
m++
for d = 2 ... N
если is_prime[d] = T
print(d)Асимптотика (без доказательства): .
Не из лекции: почему вычеркнуты ровно составные
Утверждение: после первого цикла
is_prime[k] = T⇔ простое, для всех .Простое не вычёркивается. Вычёркиваются только числа вида , где и . У такого числа есть делитель , отличный от 1 и от самого числа, — оно составное.
Составное вычёркивается. Пусть составное, — его наименьший делитель, больший 1. Он простой: иначе у него самого был бы делитель меньше, и этот делитель делил бы . Значит,
is_prime[d]никто не вычеркнет, и когда цикл дойдёт до , проверка пройдёт. Тогда переберёт и дойдёт до : . Число вычеркнуто. ∎Тонкость. Проверка
is_prime[d] = Tсмотрит в таблицу, которую алгоритм ещё строит. Это законно: если составное, его наименьший простой делитель меньше и уже обработан — к этому моменту вычеркнуто.
Не из лекции: сначала грубая оценка
Для простого внутренний цикл делает шагов. Остальное — два прохода по , это . Если завысить и считать, что внутренний цикл работает для каждого , получится
Последнее — оценка суммы гармонического ряда: , поэтому сумма не больше . Итого — уже сильно лучше, чем проверять каждое число до корня, .
Не из лекции: откуда
На самом деле внутренний цикл работает только для простых , и
Осталось доказать, что . Три шага.
Шаг 1. Произведение простых до не больше . Индукция по . При верно. Чётное не простое, произведение такое же, как для . Нечётное : каждое простое из делит числитель и не делит знаменатель, поэтому их произведение делит . А , оба слагаемых входят в , значит, . По предположению индукции произведение простых до не больше . Всего — не больше .
Логарифмируя: .
Шаг 2. . Среди чисел на делятся штук, поэтому делится на . Значит,
\ \ge\ \sum_{p \le n} \left(\frac np - 1\right) \ln p \ =\ n \sum_{p \le n} \frac{\ln p}{p} - \sum_{p \le n} \ln p.$$ По шагу 1 последняя сумма не больше $n \ln 4$. Делим на $n$. **Шаг 3. Разбиваем простые на блоки.** Блок $k$ — числа из $[2^{2^k}, 2^{2^{k+1}})$: $[2, 4)$, $[4, 16)$, $[16, 256)$, … Для $p$ из блока $k$ верно $\ln p \ge 2^k \ln 2$, поэтому $\frac1p = \frac{\ln p}{p} \cdot \frac{1}{\ln p} \le \frac{\ln p}{p} \cdot \frac{1}{2^k \ln 2}$. Сумма по блоку, по шагу 2 с $n = 2^{2^{k+1}}$: $$\sum_{p \in \text{блок } k} \frac1p \ \le\ \frac{2^{k+1} \ln 2 + 2 \ln 2}{2^k \ln 2} \ =\ 2 + 2^{1-k} \ \le\ 4.$$ Блоков, в которых есть числа до N, — столько, сколько $k$ с $2^{2^k} \le N$, то есть $k \le \log_2 \log_2 N$. Их не больше $\log_2 \log_2 N + 1$, и $$\sum_{p \le N} \frac1p \le 4 (\log_2 \log_2 N + 1) = O(\log \log N), \qquad T(N) = O(N \log \log N). \ ∎$$ Оценка точная: на самом деле $\sum_{p \le N} \frac1p \approx \ln \ln N + 0{,}26$ (теорема Мертенса, здесь без доказательства). Прогон на $N = 10^8$: внутренний цикл сделал $3{,}09 \cdot 10^8$ шагов, $N \ln \ln N \approx 2{,}91 \cdot 10^8$.
Не из лекции: память
Массив
is_primeна элемент: . На лекции не записано.
2. Что такое сортировка
Было — стало : те же элементы, переставленные.
Упорядоченно, например: . Виды порядка:
- возрастание: ;
- неубывание: ;
- убывание: ;
- невозрастание: .
Применение: фильтры, маркетплейсы, бинарный поиск (Бинарный поиск работает только на отсортированном массиве).
Не из лекции: строго или нестрого
Если в массиве есть равные элементы, расставить их по строгому возрастанию нельзя — только по неубыванию. Поэтому «отсортировать по возрастанию» на практике значит по неубыванию: все алгоритмы ниже сравнивают строго (
>,<) и равные оставляют рядом.
Подробнее — Сортировка.
3. Свойства сортировок
- Время выполнения .
- Дополнительная память — см. Время работы алгоритма.
- Устойчивость (стабильность) сортировки.
Пример: . Пометим единицы, чтобы их различать:
| Устойчивая | Неустойчивая |
|---|---|
| порядок одинаковых элементов сохранён | порядок одинаковых поменялся |
Не из лекции: зачем нужна устойчивость
Для чисел разница не видна: две единицы неотличимы. Она важна, когда сортируют записи по ключу. Пусть товары уже отсортированы по цене, и теперь их сортируют по рейтингу. Устойчивая сортировка оставит товары с одинаковым рейтингом в порядке цены, неустойчивая — перемешает.
4. Тривиальный алгоритм
[ неотсортированный ] ──▶ [ отсортированный ]- Рандомно переставляем элементы массива — .
- Проверяем, что
arrотсортирован — .
Перестановок , отсюда .
Не из лекции: поправки к оценке
- Случайная перестановка стоит , а не : надо переставить каждый из N элементов. На итог не влияет — попытка всё равно стоит .
- — среднее, а не худшее время. Если все элементы разные, отсортированная перестановка одна из , и каждая попытка попадает в неё с вероятностью . В среднем нужно попыток — отсюда . Но ничто не мешает не попасть ни разу за миллиард попыток: гарантированного времени у алгоритма нет.
- Масштаб: , . Уже на двадцати элементах — десятки лет при операций в секунду.
5. Поиск максимума
arr = [ ... ]
mx = arr[0]
for i = 0 ... N
если arr[i] > mx
mx = arr[i][////////////////|▮| ]
└──── mx ──────┘ arr[i], .
Не из лекции: почему ответ верный
Инвариант: перед шагом значение
mx— максимум средиarr[0], …,arr[i−1](на рисунке — заштрихованная часть). Шаг сравниваетarr[i]с этим максимумом и, если нужно, обновляет его. После последнего шагаmx— максимум всего массива.
Оптимизация: MAX → arr[0]
Не храним максимум в отдельной переменной, а держим его на месте arr[0]:
arr = [ ... ]
for i = 0 ... N
если arr[i] > arr[0]
swap(arr[i], arr[0])swap — обмен двух элементов через временную переменную:
t = arr[0]
arr[0] = arr[i]
arr[i] = tНе из лекции: зачем это
После цикла максимум стоит в
arr[0], а массив по-прежнему содержит те же элементы — переставленные. Это ровно один шаг сортировки: поставить на первое место нужный элемент. Сортировка выбором ниже повторяет этот шаг для каждой позиции — только ищет минимум.Почему без
tнельзя: послеarr[0] = arr[i]старое значениеarr[0]потеряно, иarr[i] = arr[0]запишет вarr[i]его же собственное значение.
6. Сортировка выбором (Selection Sort)
По возрастанию: первые позиций уже на месте, в остатке ищем минимум и ставим его следующим.
[▨|▨|▨ ... ▨| ... ]
└──── i ────┘└── поиск мин ──┘На полях — столбики по высоте: слева самых маленьких, уже по порядку, справа — вперемешку.
for i = 0 ... N
for j = i ... N
если arr[i] > arr[j]
swap(arr[i], arr[j]), . Устойчивая: нет.
Подробно о всех трёх сортировках — Квадратичные сортировки.
Не из лекции: доказательство правильности
Инвариант внешнего цикла: перед шагом элементы
arr[0], …,arr[i−1]— это наименьших элементов массива, по неубыванию.Внутренний цикл ставит в
arr[i]минимум остатка. После проверки очередного вarr[i]лежит минимум изarr[i], …,arr[j]: еслиarr[j]меньше текущегоarr[i], их меняют местами. Обмены идут только внутриarr[i], …,arr[N−1], так что остаток содержит те же элементы. После вarr[i]— минимум остатка.Этот минимум не меньше любого из первых элементов (они наименьшие), и не больше любого из оставшихся. Префикс удлинился на один элемент и остался упорядоченным. После отсортирован весь массив. ∎
Не из лекции: точное число сравнений
Внутренний цикл на шаге делает сравнений (при — сам с собой, лишнее, но так на доске). Всего
— на любом входе, даже на уже отсортированном: цикл не знает, что минимум уже на месте. Прогон на : 500 500 сравнений.
Не из лекции: почему неустойчива
Обмен
arr[i]с далёкимarr[j]перебрасываетarr[i]через все элементы между ними — в том числе через равный ему. Разбор примера:
- : — ? нет; — ? да, обмен: . Двойка перепрыгнула через .
- : ? нет. Порядок двоек так и остался обратным.
Прогон на C++ даёт то же самое.
Не из лекции: на доске — не классический вариант
На доске
swapвыполняется внутри внутреннего цикла, каждый раз, как нашёлся элемент меньшеarr[i]. Классическая сортировка выбором сначала находит индекс минимума и делает один обмен на шаг . Сравнений столько же, , обменов — не больше вместо . Классический вариант тоже неустойчив, контрпример тот же.
7. Сортировка вставками (Insertion Sort)
По возрастанию: левая часть уже отсортирована, очередной элемент «проваливается» влево, пока слева от него элемент больше.
[//////////////|▮| ]
←────┘ jarr = [ ... ]
for i = 0 ... N
j = i + 1
while (j > 0 и arr[j] < arr[j−1])
swap(arr[j], arr[j−1])
j = j − 1, . Устойчивость: да. На рисунке — два одинаковых столбика рядом: равные элементы друг через друга не перепрыгивают.
Выход за границу массива
При (последняя итерация полуинтервала ) получается
j = N, и условиеwhileчитаетarr[N]— за пределами массива. В C++ это Неопределённое поведение; прогон с AddressSanitizer сообщаетheap-buffer-overflow. Внешний цикл должен идти на одну итерацию меньше:for i = 0 ... N−1. Или, как чаще пишут:for i = 1 ... N,j = i.
Не из лекции: доказательство правильности
Инвариант: перед шагом отсортированы
arr[0], …,arr[i]. Вначале это один элемент — отсортирован.Шаг вставляет
arr[i+1]. Пока он строго меньше левого соседа, они меняются местами. Цикл останавливается в одном из двух случаев:
- — элемент меньше всех, стоит первым;
arr[j−1] ≤ arr[j]— слева всё не больше его.Справа от него стоят элементы, через которые он прошёл, — они больше его и по-прежнему упорядочены (каждый сдвинулся на одну позицию вправо). Значит,
arr[0], …,arr[i+1]отсортированы. ∎
Не из лекции: сколько обменов
Инверсия — пара позиций , где
arr[p] > arr[q]. Обмен двух соседних элементов, стоящих в неправильном порядке, убирает ровно одну инверсию и других не трогает. Значит, обменов ровно столько, сколько инверсий во входе.
- Худший случай — массив по убыванию: инверсий , это .
- Лучший — уже отсортированный: обменов нет, на каждом шаге одно сравнение, всего , это .
Прогон на : по убыванию — 499 500 сравнений, по возрастанию — 999. В отличие от выбора, вставки работают быстро на почти отсортированных данных.
Не из лекции: почему устойчива
Обмен происходит только при строгом
arr[j] < arr[j−1]. Элемент останавливается перед первым равным ему и никогда не перепрыгивает через него. Элементы движутся только обменом соседей, поэтому равные не могут поменяться местами иначе как напрямую — а напрямую их не меняют. Если написать≤, сортировка останется правильной, но станет неустойчивой.
8. Сортировка пузырьком (Bubble Sort)
Каждый проход «всплывает» максимум: он уезжает вправо, на позицию . Справа копится отсортированная часть.
[ ~~~~~ max ~~~~~▶|▮|//////////]
N−i └─── i ───┘for i = 0 ... N
for j = 0 ... N−i−1
если arr[j] > arr[j+1]
swap(arr[j], arr[j+1]), . Устойчивость: да.
Не из лекции: доказательство правильности
Утверждение о проходе. Во время прохода после сравнения на шаге в
arr[j+1]лежит максимум изarr[0], …,arr[j+1]. Индукция по : еслиarr[j]— максимум первых элементов и он большеarr[j+1], обмен утащит его вправо; если не больше — максимум и такarr[j+1].Инвариант внешнего цикла: после прохода последние позиций занимают наибольших элементов, по неубыванию. Проход заканчивается на и приносит в
arr[N−i−1]максимум неотсортированной части. Он не больше уже стоящих справа — те наибольшие. ∎Последний проход пустой:
j = 0 ... −1.
Не из лекции: число сравнений и устойчивость
Сравнений на любом входе. На доске оценка записана символом, похожим на ; верны обе, точный порядок — .
Обменов, как и у вставок, ровно столько, сколько инверсий: каждый обмен соседей в неправильном порядке убирает одну.
Устойчива по той же причине, что вставки: меняются только соседи и только при строгом
>. Равные соседи остаются как были.Частая оптимизация: если за проход не было ни одного обмена, массив уже отсортирован, и можно остановиться. Тогда на отсортированном входе — .
Сравнение
| Устойчива | Сравнений (не из лекции) | |||
|---|---|---|---|---|
| Выбором | нет | всегда | ||
| Вставками | да | от до | ||
| Пузырьком | да | всегда |
Проверено прогоном на 2000 случайных массивах с повторами: все три сортируют правильно, неустойчив только выбор.
Грабли
- Сортировка вставками с доски на последней итерации читает
arr[N]. Внешний цикл — доN − 1. swapбез временной переменной теряет одно из значений.- Запись
a ... bне говорит, входит лиb: в решете входит, в сортировках нет. Проверять по индексам. - Тривиальная сортировка не имеет гарантированного времени — только в среднем.
≤вместо<во вставках (или≥вместо>в пузырьке) ломает устойчивость.
После этой лекции должно получаться
- написать решето Эратосфена и доказать, что вычеркнуты ровно составные;
- объяснить, откуда в решете и почему грубая оценка даёт ;
- определить сортировку и устойчивость, привести пример неустойчивой;
- написать сортировки выбором, вставками и пузырьком, доказать их через инварианты и посчитать сравнения;
- объяснить, почему выбор неустойчив, а вставки и пузырёк — устойчивы;
- найти выход за границу в сортировке вставками с доски.