Add ru version (#1865)

* Add Russian docs site baseline

* Add Russian localized codebase

* Polish Russian code wording

* Update ru code translation.

* Update code translation and chapter covers.

* Fix pythontutor extraction.

* Add README and landing page.

* placeholder of profiles

* Use figures of English version

* Remove chapter paperbook
This commit is contained in:
Yudong Jin
2026-03-28 04:24:07 +08:00
committed by GitHub
parent 2ca570cc33
commit 772183705e
1958 changed files with 108186 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+59
View File
@@ -0,0 +1,59 @@
# Сортировка пузырьком
<u>Сортировка пузырьком (bubble sort)</u> сортирует массив за счет непрерывного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма.
Как показано на рисунке ниже, процесс "всплытия" можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если "левый элемент > правый элемент", меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.
=== "<1>"
![Моделирование пузырька через обмен элементов](bubble_sort.assets/bubble_operation_step1.png)
=== "<2>"
![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png)
=== "<3>"
![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png)
=== "<4>"
![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png)
=== "<5>"
![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png)
=== "<6>"
![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png)
=== "<7>"
![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png)
## Алгоритм
Пусть длина массива равна $n$ ; тогда шаги сортировки пузырьком показаны на рисунке ниже.
1. Сначала выполнить один проход "всплытия" по $n$ элементам, **переместив максимальный элемент массива на правильную позицию**.
2. Затем выполнить "всплытие" по оставшимся $n - 1$ элементам, **переместив второй по величине элемент на правильную позицию**.
3. Продолжать по аналогии; после $n - 1$ раундов "всплытия" **первые $n - 1$ по величине элементы окажутся на правильных позициях**.
4. Оставшийся единственный элемент обязательно является минимальным, сортировать его уже не нужно, поэтому сортировка завершена.
![Процесс сортировки пузырьком](bubble_sort.assets/bubble_sort_overview.png)
Пример кода:
```src
[file]{bubble_sort}-[class]{}-[func]{bubble_sort}
```
## Оптимизация эффективности
Мы замечаем, что если в каком-либо раунде "всплытия" не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг `flag` для отслеживания этой ситуации и немедленного выхода.
После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны $O(n^2)$ ; однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность $O(n)$ .
```src
[file]{bubble_sort}-[class]{}-[func]{bubble_sort_with_flag}
```
## Характеристики алгоритма
- **Временная сложность равна $O(n^2)$, алгоритм адаптивен**: длины диапазонов, проходящих "всплытие" в разных раундах, последовательно равны $n - 1$, $n - 2$, $\dots$, $2$, $1$ , а их сумма равна $(n - 1) n / 2$ . После добавления оптимизации с `flag` лучшая временная сложность может достигать $O(n)$ .
- **Пространственная сложность равна $O(1)$, сортировка выполняется на месте**: указатели $i$ и $j$ используют константный объем дополнительной памяти.
- **Стабильная сортировка**: поскольку при "всплытии" равные элементы не обмениваются местами.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+45
View File
@@ -0,0 +1,45 @@
# Блочная сортировка
Рассмотренные выше алгоритмы сортировки относятся к "сортировкам на основе сравнений": они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше $O(n \log n)$ . Далее мы рассмотрим несколько "сортировок без сравнений", чья временная сложность может достигать линейного порядка.
<u>Блочная сортировка (bucket sort)</u> является типичным применением стратегии "разделяй и властвуй". Она задает несколько упорядоченных по диапазонам блоков, каждый блок соответствует некоторому диапазону значений; затем данные равномерно распределяются по блокам, внутри каждого блока выполняется сортировка, а в конце результаты блоков объединяются по порядку.
## Алгоритм
Рассмотрим массив длины $n$, элементы которого являются числами с плавающей запятой из диапазона $[0, 1)$ . Процесс блочной сортировки показан на рисунке ниже.
1. Инициализировать $k$ блоков и распределить $n$ элементов по этим $k$ блокам.
2. Отсортировать каждый блок по отдельности (здесь используется встроенная функция сортировки языка программирования).
3. Объединить результаты в порядке следования блоков от меньшего к большему.
![Процесс блочной сортировки](bucket_sort.assets/bucket_sort_overview.png)
Код приведен ниже:
```src
[file]{bucket_sort}-[class]{}-[func]{bucket_sort}
```
## Характеристики алгоритма
Блочная сортировка подходит для обработки очень больших объемов данных. Например, если вход содержит 1 миллион элементов и из-за ограничений памяти система не может загрузить их все сразу, можно разбить данные на 1000 блоков, отсортировать каждый блок отдельно, а затем объединить результаты.
- **Временная сложность равна $O(n + k)$** : если элементы распределены по блокам равномерно, то в каждом блоке будет $\frac{n}{k}$ элементов. Если сортировка одного блока требует $O(\frac{n}{k} \log\frac{n}{k})$ времени, то сортировка всех блоков потребует $O(n \log\frac{n}{k})$ времени. **Когда число блоков $k$ достаточно велико, временная сложность приближается к $O(n)$** . На объединение результатов требуется $O(n + k)$ времени, потому что нужно пройти по всем блокам и элементам. В худшем случае все данные попадут в один блок, и если сортировка этого блока использует $O(n^2)$ времени, общая сложность также станет $O(n^2)$ .
- **Пространственная сложность равна $O(n + k)$, сортировка не выполняется на месте**: требуются дополнительные блоки в количестве $k$ и место для всех $n$ элементов внутри них.
- Является ли блочная сортировка стабильной, зависит от того, стабилен ли алгоритм сортировки внутри каждого блока.
## Как добиться равномерного распределения
Теоретически временная сложность блочной сортировки может достигать $O(n)$ ; **ключ к этому - как можно более равномерно распределить элементы по блокам**. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на Taobao по 10 блокам цен, количество товаров дешевле 100 юаней может быть очень большим, а товаров дороже 1000 юаней - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.
Чтобы добиться более равномерного распределения, можно сначала задать грубую линию раздела и приблизительно распределить данные по 3 блокам. **После этого блоки с большим числом товаров можно снова делить на 3 блока и продолжать процесс до тех пор, пока число элементов в каждом блоке не станет примерно одинаковым**.
Как показано на рисунке ниже, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока; конкретную схему разбиения можно выбирать в зависимости от свойств данных.
![Рекурсивное разбиение по блокам](bucket_sort.assets/scatter_in_buckets_recursively.png)
Если нам заранее известна вероятностная модель распределения цен товаров, **то границы каждого блока можно задавать в соответствии с этим распределением**. Важно отметить, что фактическое распределение данных не обязательно специально измерять - его можно приблизить некоторой вероятностной моделью исходя из свойств данных.
Как показано на рисунке ниже, если предположить, что цены товаров подчиняются нормальному распределению, то можно разумно задать интервалы цен и тем самым распределить товары по блокам достаточно равномерно.
![Разбиение блоков по вероятностному распределению](bucket_sort.assets/scatter_in_buckets_distribution.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+84
View File
@@ -0,0 +1,84 @@
# Сортировка подсчетом
<u>Сортировка подсчетом (counting sort)</u> реализует сортировку за счет подсчета количества элементов и обычно используется для массивов целых чисел.
## Простая реализация
Сначала рассмотрим простой пример. Дан массив `nums` длины $n$ , элементы которого являются "неотрицательными целыми числами". Общий процесс сортировки подсчетом показан на рисунке ниже.
1. Пройти по массиву, найти в нем максимальное число, обозначить его как $m$ , а затем создать вспомогательный массив `counter` длины $m + 1$ .
2. **С помощью `counter` подсчитать, сколько раз каждое число встречается в `nums`**; при этом `counter[num]` хранит число вхождений значения `num` . Делается это просто: достаточно пройти по `nums` (пусть текущее число равно `num` ) и на каждом шаге увеличить `counter[num]` на $1$ .
3. **Поскольку индексы массива `counter` изначально упорядочены, можно считать, что все числа уже отсортированы**. Далее остается пройти по `counter` и в соответствии с числом вхождений записать значения обратно в `nums` в порядке возрастания.
![Процесс сортировки подсчетом](counting_sort.assets/counting_sort_overview.png)
Код приведен ниже:
```src
[file]{counting_sort}-[class]{}-[func]{counting_sort_naive}
```
!!! note "Связь между сортировкой подсчетом и блочной сортировкой"
Если посмотреть на сортировку подсчетом с точки зрения блочной сортировки, то каждый индекс массива `counter` можно рассматривать как отдельный блок, а процесс подсчета - как распределение элементов по соответствующим блокам. По сути, сортировка подсчетом является частным случаем блочной сортировки для целочисленных данных.
## Полная реализация
Внимательный читатель мог заметить, что **если входные данные представлены объектами, то описанный выше шаг `3.` перестает работать**. Например, если входными данными являются объекты товаров и мы хотим отсортировать их по цене (полю класса), то описанный алгоритм сможет выдать только отсортированный ряд цен, но не исходные объекты в нужном порядке.
Как же получить корректный порядок исходных данных? Сначала вычислим "префиксную сумму" массива `counter` . Как следует из названия, префиксная сумма в индексе `i` , обозначаемая как `prefix[i]` , равна сумме первых `i` элементов массива:
$$
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
$$
**Префиксная сумма имеет четкий смысл: `prefix[num] - 1` обозначает индекс последнего вхождения элемента `num` в результирующем массиве `res`**. Это очень важная информация, потому что она указывает, в какую позицию результирующего массива должен попасть каждый элемент. Далее мы проходим исходный массив `nums` в обратном порядке и на каждой итерации для очередного элемента `num` выполняем два действия.
1. Записать `num` в массив `res` по индексу `prefix[num] - 1` .
2. Уменьшить префиксную сумму `prefix[num]` на $1$ , чтобы получить индекс следующего размещения элемента `num` .
После завершения прохода массив `res` будет содержать отсортированный результат; остается только переписать `res` обратно в `nums` . Полный процесс сортировки подсчетом показан на рисунке ниже.
=== "<1>"
![Шаги сортировки подсчетом](counting_sort.assets/counting_sort_step1.png)
=== "<2>"
![counting_sort_step2](counting_sort.assets/counting_sort_step2.png)
=== "<3>"
![counting_sort_step3](counting_sort.assets/counting_sort_step3.png)
=== "<4>"
![counting_sort_step4](counting_sort.assets/counting_sort_step4.png)
=== "<5>"
![counting_sort_step5](counting_sort.assets/counting_sort_step5.png)
=== "<6>"
![counting_sort_step6](counting_sort.assets/counting_sort_step6.png)
=== "<7>"
![counting_sort_step7](counting_sort.assets/counting_sort_step7.png)
=== "<8>"
![counting_sort_step8](counting_sort.assets/counting_sort_step8.png)
Код реализации сортировки подсчетом приведен ниже:
```src
[file]{counting_sort}-[class]{}-[func]{counting_sort}
```
## Характеристики алгоритма
- **Временная сложность равна $O(n + m)$, алгоритм не является адаптивным** : необходимо пройти по `nums` и по `counter` , а оба этих прохода занимают линейное время. Обычно выполняется $n \gg m$ , поэтому временная сложность стремится к $O(n)$ .
- **Пространственная сложность равна $O(n + m)$, сортировка не выполняется на месте**: используются массивы `res` и `counter` длины $n$ и $m$ соответственно.
- **Стабильная сортировка**: порядок заполнения `res` идет "справа налево", поэтому обратный проход по `nums` позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по `nums` тоже даст правильный результат сортировки, но он будет нестабильным.
## Ограничения
На этом этапе сортировка подсчетом может показаться очень изящной: она позволяет эффективно сортировать данные, опираясь только на подсчет числа вхождений. Однако условия ее применения довольно строгие.
**Сортировка подсчетом применима только к неотрицательным целым числам**. Чтобы использовать ее для других типов данных, нужно убедиться, что эти данные можно преобразовать в неотрицательные целые числа и что при преобразовании относительный порядок элементов не изменится. Например, для массива целых чисел с отрицательными значениями можно сначала прибавить ко всем числам константу, превратив их в положительные, затем выполнить сортировку и после этого преобразовать значения обратно.
**Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений невелик**. Например, в приведенном выше примере $m$ не должно быть слишком большим, иначе будет занято слишком много памяти. А когда $n \ll m$ , сортировка подсчетом использует $O(m)$ времени и может оказаться медленнее, чем алгоритмы сортировки с $O(n \log n)$ .
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+73
View File
@@ -0,0 +1,73 @@
# Пирамидальная сортировка
!!! tip
Перед чтением этого раздела убедитесь, что вы уже изучили главу "Куча".
<u>Пирамидальная сортировка (heap sort)</u> - это эффективный алгоритм сортировки, основанный на структуре данных "куча". Для его реализации можно использовать уже изученные нами "построение кучи" и "извлечение элементов из кучи".
1. Подать на вход массив и построить из него мин-кучу; в этот момент минимальный элемент будет находиться в вершине кучи.
2. Непрерывно выполнять извлечение из кучи и по порядку записывать извлеченные элементы - так получится последовательность, отсортированная по возрастанию.
Хотя этот метод работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию.
## Алгоритм
Пусть длина массива равна $n$ ; тогда процесс пирамидальной сортировки показан на рисунке ниже.
1. Подать на вход массив и построить из него макс-кучу. После этого максимальный элемент окажется в вершине кучи.
2. Обменять элемент в вершине кучи (первый элемент) с элементом внизу кучи (последний элемент). После обмена длина кучи уменьшается на $1$ , а число уже отсортированных элементов увеличивается на $1$ .
3. Начиная с вершины, выполнить просеивание вниз (sift down) сверху вниз. После этого свойство кучи будет восстановлено.
4. Циклически повторять шаг `2.` и шаг `3.` . После $n - 1$ раундов массив будет полностью отсортирован.
!!! tip
На самом деле операция извлечения из кучи тоже включает шаг `2.` и шаг `3.` , только дополнительно содержит действие по удалению элемента.
=== "<1>"
![Шаги пирамидальной сортировки](heap_sort.assets/heap_sort_step1.png)
=== "<2>"
![heap_sort_step2](heap_sort.assets/heap_sort_step2.png)
=== "<3>"
![heap_sort_step3](heap_sort.assets/heap_sort_step3.png)
=== "<4>"
![heap_sort_step4](heap_sort.assets/heap_sort_step4.png)
=== "<5>"
![heap_sort_step5](heap_sort.assets/heap_sort_step5.png)
=== "<6>"
![heap_sort_step6](heap_sort.assets/heap_sort_step6.png)
=== "<7>"
![heap_sort_step7](heap_sort.assets/heap_sort_step7.png)
=== "<8>"
![heap_sort_step8](heap_sort.assets/heap_sort_step8.png)
=== "<9>"
![heap_sort_step9](heap_sort.assets/heap_sort_step9.png)
=== "<10>"
![heap_sort_step10](heap_sort.assets/heap_sort_step10.png)
=== "<11>"
![heap_sort_step11](heap_sort.assets/heap_sort_step11.png)
=== "<12>"
![heap_sort_step12](heap_sort.assets/heap_sort_step12.png)
В реализации кода используется та же функция просеивания сверху вниз `sift_down()`, что и в главе "Куча". Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции `sift_down()` нужно передавать параметр длины $n$ , чтобы указать текущую эффективную длину кучи. Код приведен ниже:
```src
[file]{heap_sort}-[class]{}-[func]{heap_sort}
```
## Характеристики алгоритма
- **Временная сложность равна $O(n \log n)$, алгоритм не является адаптивным**: построение кучи занимает $O(n)$ времени. Извлечение максимального элемента из кучи имеет временную сложность $O(\log n)$ и выполняется $n - 1$ раз.
- **Пространственная сложность равна $O(1)$, сортировка выполняется на месте**: несколько переменных-указателей используют $O(1)$ памяти. Обмен элементов и операции просеивания выполняются прямо в исходном массиве.
- **Нестабильная сортировка**: при обмене вершины кучи и нижнего элемента относительный порядок равных элементов может измениться.
+9
View File
@@ -0,0 +1,9 @@
# Сортировка
![Сортировка](../assets/covers/chapter_sorting.jpg)
!!! abstract
Сортировка упорядочивает хаотичные данные и позволяет быстрее находить закономерности.
За кажущейся простотой скрывается целая группа алгоритмов с разными достоинствами и ограничениями.
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+46
View File
@@ -0,0 +1,46 @@
# Сортировка вставками
<u>Сортировка вставками (insertion sort)</u> - это простой алгоритм сортировки, принцип которого очень похож на ручное упорядочивание колоды карт.
Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он поочередно сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию.
На рисунке ниже показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как `base` ; нам нужно сдвинуть все элементы от целевого индекса до `base` на одну позицию вправо, а затем присвоить `base` значение в целевом индексе.
![Одна операция вставки](insertion_sort.assets/insertion_operation.png)
## Алгоритм
Общий процесс сортировки вставками показан на рисунке ниже.
1. В начальном состоянии отсортирован только первый элемент массива.
2. Выбрать второй элемент массива как `base` ; после вставки в правильную позицию **первые 2 элемента массива окажутся отсортированными**.
3. Выбрать третий элемент как `base` ; после вставки в правильную позицию **первые 3 элемента массива окажутся отсортированными**.
4. Продолжать по аналогии; в последнем раунде в качестве `base` берется последний элемент, и после его вставки **все элементы массива будут отсортированы**.
![Процесс сортировки вставками](insertion_sort.assets/insertion_sort_overview.png)
Пример кода:
```src
[file]{insertion_sort}-[class]{}-[func]{insertion_sort}
```
## Характеристики алгоритма
- **Временная сложность равна $O(n^2)$, алгоритм адаптивен**: в худшем случае каждой операции вставки требуется соответственно $n - 1$, $n-2$, $\dots$, $2$, $1$ итераций, а их сумма равна $(n - 1) n / 2$ , поэтому временная сложность равна $O(n^2)$ . Если входные данные уже упорядочены, операция вставки завершается раньше. Когда входной массив полностью отсортирован, сортировка вставками достигает лучшей временной сложности $O(n)$ .
- **Пространственная сложность равна $O(1)$, сортировка выполняется на месте**: указатели $i$ и $j$ используют константный объем дополнительной памяти.
- **Стабильная сортировка**: в процессе вставки элементы помещаются справа от равных им элементов, поэтому их относительный порядок не меняется.
## Преимущества сортировки вставками
Временная сложность сортировки вставками равна $O(n^2)$ , а у быстрой сортировки, которую мы скоро изучим, временная сложность равна $O(n \log n)$ . Несмотря на более высокую асимптотическую сложность, **на малых объемах данных сортировка вставками обычно работает быстрее**.
Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня $O(n \log n)$ , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии "разделяй и властвуй" и обычно включают больше элементарных вычислений. Когда объем данных мал, значения $n^2$ и $n \log n$ близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.
На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии "разделяй и властвуй", например быструю сортировку; для коротких массивов сразу использовать сортировку вставками.
Хотя сортировка пузырьком, выбором и вставками имеют одинаковую временную сложность $O(n^2)$ , в реальных задачах **сортировка вставками используется заметно чаще, чем сортировка пузырьком и сортировка выбором**. Основные причины таковы.
- Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции; сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому **вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками**.
- Временная сложность сортировки выбором в любом случае равна $O(n^2)$ . **Если входные данные уже частично упорядочены, сортировка вставками обычно эффективнее сортировки выбором**.
- Сортировка выбором нестабильна, поэтому ее нельзя использовать для многоуровневой сортировки.
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+73
View File
@@ -0,0 +1,73 @@
# Сортировка слиянием
<u>Сортировка слиянием (merge sort)</u> - это алгоритм сортировки на основе стратегии "разделяй и властвуй", который включает этапы "разделения" и "слияния", показанные на рисунке ниже.
1. **Этап разделения**: массив рекурсивно разбивается от середины, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов.
2. **Этап слияния**: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; левые и правые короткие упорядоченные массивы непрерывно объединяются в более длинный упорядоченный массив, пока процесс не завершится.
![Этапы разделения и слияния в сортировке слиянием](merge_sort.assets/merge_sort_overview.png)
## Алгоритм
Как показано на рисунке ниже, на этапе "разделения" массив рекурсивно разбивается сверху вниз по середине на два подмассива.
1. Вычислить середину массива `mid` и рекурсивно разделить левый подмассив (интервал `[left, mid]` ) и правый подмассив (интервал `[mid + 1, right]` ).
2. Рекурсивно повторять шаг `1.` , пока длина подмассива не станет равной 1.
Этап "слияния" снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.
=== "<1>"
![Шаги сортировки слиянием](merge_sort.assets/merge_sort_step1.png)
=== "<2>"
![merge_sort_step2](merge_sort.assets/merge_sort_step2.png)
=== "<3>"
![merge_sort_step3](merge_sort.assets/merge_sort_step3.png)
=== "<4>"
![merge_sort_step4](merge_sort.assets/merge_sort_step4.png)
=== "<5>"
![merge_sort_step5](merge_sort.assets/merge_sort_step5.png)
=== "<6>"
![merge_sort_step6](merge_sort.assets/merge_sort_step6.png)
=== "<7>"
![merge_sort_step7](merge_sort.assets/merge_sort_step7.png)
=== "<8>"
![merge_sort_step8](merge_sort.assets/merge_sort_step8.png)
=== "<9>"
![merge_sort_step9](merge_sort.assets/merge_sort_step9.png)
=== "<10>"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png)
Нетрудно заметить, что порядок рекурсии в сортировке слиянием совпадает с порядком рекурсии при постфиксном обходе бинарного дерева.
- **Постфиксный обход**: сначала рекурсивно обходится левое поддерево, затем правое поддерево, а в конце обрабатывается корневой узел.
- **Сортировка слиянием**: сначала рекурсивно обрабатывается левый подмассив, затем правый подмассив, а в конце выполняется слияние.
Реализация сортировки слиянием показана в коде ниже. Обратите внимание: в `nums` объединяемый интервал равен `[left, right]` , а соответствующий интервал в `tmp` равен `[0, right - left]` .
```src
[file]{merge_sort}-[class]{}-[func]{merge_sort}
```
## Характеристики алгоритма
- **Временная сложность равна $O(n \log n)$, алгоритм не является адаптивным**: этап разделения создает дерево рекурсии высоты $\log n$ , а суммарное число операций слияния на каждом уровне равно $n$ , поэтому общая временная сложность составляет $O(n \log n)$ .
- **Пространственная сложность равна $O(n)$, сортировка не выполняется на месте**: глубина рекурсии равна $\log n$ , из-за чего требуется $O(\log n)$ памяти под стек вызовов. Для этапа слияния нужен вспомогательный массив, поэтому дополнительно используется $O(n)$ памяти.
- **Стабильная сортировка**: в процессе слияния относительный порядок равных элементов не меняется.
## Сортировка связного списка
Для связных списков сортировка слиянием имеет заметное преимущество перед другими алгоритмами сортировки: **пространственную сложность задачи сортировки списка можно оптимизировать до $O(1)$**.
- **Этап разделения**: работу по разбиению списка можно реализовать с помощью "итерации" вместо "рекурсии", тем самым устранив расход памяти на стек вызовов.
- **Этап слияния**: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список.
Детали реализации достаточно сложны; заинтересованные читатели могут изучить соответствующие материалы самостоятельно.
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+100
View File
@@ -0,0 +1,100 @@
# Быстрая сортировка
<u>Быстрая сортировка (quick sort)</u> - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй"; он работает эффективно и применяется очень широко.
Ключевая операция быстрой сортировки - это "разделение с опорным элементом". Ее цель такова: выбрать некоторый элемент массива в качестве "опорного" и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке ниже.
1. Выбрать самый левый элемент массива как опорный и инициализировать два указателя `i` и `j` , направленные на левую и правую границы массива.
2. Запустить цикл, в котором `i` и `j` ищут соответственно первый элемент, больший опорного, и первый элемент, меньший опорного, после чего эти два элемента меняются местами.
3. Повторять шаг `2.` , пока указатели `i` и `j` не встретятся, а затем обменять опорный элемент с элементом на границе двух подмассивов.
=== "<1>"
![Шаги разделения с опорным элементом](quick_sort.assets/pivot_division_step1.png)
=== "<2>"
![pivot_division_step2](quick_sort.assets/pivot_division_step2.png)
=== "<3>"
![pivot_division_step3](quick_sort.assets/pivot_division_step3.png)
=== "<4>"
![pivot_division_step4](quick_sort.assets/pivot_division_step4.png)
=== "<5>"
![pivot_division_step5](quick_sort.assets/pivot_division_step5.png)
=== "<6>"
![pivot_division_step6](quick_sort.assets/pivot_division_step6.png)
=== "<7>"
![pivot_division_step7](quick_sort.assets/pivot_division_step7.png)
=== "<8>"
![pivot_division_step8](quick_sort.assets/pivot_division_step8.png)
=== "<9>"
![pivot_division_step9](quick_sort.assets/pivot_division_step9.png)
После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив; при этом выполняется условие "любой элемент левого подмассива $\leq$ опорный элемент $\leq$ любой элемент правого подмассива". Следовательно, далее нам нужно лишь отсортировать эти два подмассива.
!!! note "Стратегия divide and conquer в быстрой сортировке"
По сути, разделение с опорным элементом сводит задачу сортировки длинного массива к двум задачам сортировки более коротких массивов.
```src
[file]{quick_sort}-[class]{quick_sort}-[func]{partition}
```
## Алгоритм
Общий процесс быстрой сортировки показан на рисунке ниже.
1. Сначала выполнить "разделение с опорным элементом" для исходного массива и получить неотсортированные левый и правый подмассивы.
2. Затем рекурсивно выполнить "разделение с опорным элементом" для левого и правого подмассивов.
3. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1; после этого сортировка всего массива будет завершена.
![Процесс быстрой сортировки](quick_sort.assets/quick_sort_overview.png)
```src
[file]{quick_sort}-[class]{quick_sort}-[func]{quick_sort}
```
## Характеристики алгоритма
- **Временная сложность равна $O(n \log n)$, алгоритм не является адаптивным**: в среднем глубина рекурсии при разделении равна $\log n$ , а суммарное число циклов на каждом уровне равно $n$ , поэтому общая сложность составляет $O(n \log n)$ . В худшем случае каждое разделение делит массив длины $n$ на подмассивы длины $0$ и $n - 1$ ; тогда глубина рекурсии достигает $n$ , на каждом уровне выполняется $n$ операций, и общая временная сложность вырождается в $O(n^2)$ .
- **Пространственная сложность равна $O(n)$, сортировка выполняется на месте**: если входной массив полностью отсортирован в обратном порядке, глубина рекурсии достигает худшего случая $n$ , что требует $O(n)$ памяти под стек вызовов. При этом сама сортировка выполняется в исходном массиве без дополнительного массива.
- **Нестабильная сортировка**: на последнем шаге разделения опорный элемент может быть обменян вправо от равного ему элемента.
## Почему быстрая сортировка быстрая
Уже по названию понятно, что быстрая сортировка должна иметь преимущества по эффективности. Хотя ее средняя временная сложность совпадает со сложностью "сортировки слиянием" и "пирамидальной сортировки", на практике быстрая сортировка обычно работает быстрее. Основные причины таковы.
- **Вероятность худшего случая очень мала**: хотя худшая временная сложность быстрой сортировки равна $O(n^2)$ и она не так стабильна, как сортировка слиянием, в подавляющем большинстве случаев она работает за $O(n \log n)$ .
- **Высокая эффективность использования кэша**: при выполнении разделения система может загрузить весь подмассив в кэш, поэтому доступ к элементам оказывается быстрым. Алгоритмы вроде "пирамидальной сортировки" требуют скачкообразного доступа к элементам и таким свойством не обладают.
- **Небольшой константный множитель в сложности**: среди трех перечисленных алгоритмов у быстрой сортировки обычно меньше всего сравнений, присваиваний и обменов. Это похоже на причину, по которой "сортировка вставками" часто быстрее "сортировки пузырьком".
## Оптимизация выбора опорного элемента
**На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться**. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет $n - 1$ , а длина правого - $0$ . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину $0$ , стратегия divide and conquer потеряет смысл, а быстрая сортировка выродится в нечто близкое к "сортировке пузырьком".
Чтобы по возможности избежать такого сценария, **мы можем улучшить стратегию выбора опорного элемента в процедуре разделения**. Например, можно выбирать случайный элемент массива как опорный. Однако если не повезет и каждый раз будет выбираться неудачный опорный элемент, производительность все равно останется неудовлетворительной.
Нужно учитывать, что языки программирования обычно генерируют "псевдослучайные числа". Если специально построить тестовый пример под такую последовательность, эффективность быстрой сортировки все равно может деградировать.
Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и **использовать медиану этих трех значений как опорный элемент**. Благодаря этому вероятность того, что опорный элемент окажется "не слишком маленьким и не слишком большим", заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до $O(n^2)$ существенно уменьшается.
Пример кода:
```src
[file]{quick_sort}-[class]{quick_sort_median}-[func]{partition}
```
## Оптимизация глубины рекурсии
**На некоторых входных данных быстрая сортировка может занимать слишком много памяти**. Рассмотрим полностью отсортированный входной массив. Пусть длина текущего подмассива в рекурсии равна $m$ ; тогда после каждого разделения будут получаться левый подмассив длины $0$ и правый подмассив длины $m - 1$ . Это означает, что на каждом уровне размер задачи уменьшается совсем немного (лишь на один элемент), а высота дерева рекурсии достигает $n - 1$ , поэтому требуется $O(n)$ памяти под стек вызовов.
Чтобы избежать накопления стековых кадров, после каждого разделения можно сравнивать длины двух подмассивов и **рекурсивно обрабатывать только более короткий из них**. Поскольку длина короткого подмассива не превысит $n / 2$ , такой подход гарантирует, что глубина рекурсии не превысит $\log n$ , а худшая пространственная сложность будет оптимизирована до $O(\log n)$ . Код приведен ниже:
```src
[file]{quick_sort}-[class]{quick_sort_tail_call}-[func]{quick_sort}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+41
View File
@@ -0,0 +1,41 @@
# Поразрядная сортировка
В предыдущем разделе мы познакомились с сортировкой подсчетом: она подходит для случаев, когда объем данных $n$ велик, а диапазон значений $m$ сравнительно мал. Предположим теперь, что нужно отсортировать $n = 10^6$ студенческих идентификаторов, причем каждый идентификатор является $8$-значным числом. Тогда диапазон данных $m = 10^8$ оказывается очень большим; сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.
<u>Поразрядная сортировка (radix sort)</u> по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. Поверх этого поразрядная сортировка использует иерархию разрядов числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат.
## Алгоритм
Рассмотрим пример со студенческими номерами: будем считать, что младший разряд имеет номер $1$ , а старший - номер $8$ . Тогда процесс поразрядной сортировки показан на рисунке ниже.
1. Инициализировать номер разряда $k = 1$ .
2. Выполнить "сортировку подсчетом" по $k$-му разряду студенческого номера. После этого данные будут упорядочены по $k$-му разряду по возрастанию.
3. Увеличить $k$ на $1$ и вернуться к шагу `2.` , продолжая процесс, пока сортировка не будет выполнена для всех разрядов.
![Процесс поразрядной сортировки](radix_sort.assets/radix_sort_overview.png)
Ниже разберем реализацию кода. Для числа $x$ в системе счисления с основанием $d$ получить его $k$-й разряд $x_k$ можно по формуле:
$$
x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
$$
где $\lfloor a \rfloor$ обозначает округление числа $a$ вниз, а $\bmod \: d$ означает взятие остатка по модулю $d$ . Для студенческих идентификаторов выполняется $d = 10$ и $k \in [1, 8]$ .
Кроме того, нам нужно слегка изменить код сортировки подсчетом, чтобы он мог сортировать числа по их $k$-му разряду:
```src
[file]{radix_sort}-[class]{}-[func]{radix_sort}
```
!!! question "Почему сортировка выполняется от младшего разряда к старшему?"
В последовательных раундах сортировки результаты более позднего раунда перекрывают результаты предыдущего. Например, если после первого раунда получилось $a < b$ , а после второго - $a > b$ , то именно результат второго раунда станет окончательным. Поскольку старшие разряды имеют более высокий приоритет, сначала нужно сортировать по младшим разрядам, а затем по старшим.
## Характеристики алгоритма
По сравнению с сортировкой подсчетом поразрядная сортировка подходит для случаев с большим диапазоном чисел, **но только при условии, что данные можно представить в виде чисел фиксированной длины и число разрядов не слишком велико**. Например, числа с плавающей запятой плохо подходят для поразрядной сортировки, потому что число разрядов $k$ слишком велико и может привести к ситуации $O(nk) \gg O(n^2)$ .
- **Временная сложность равна $O(nk)$, алгоритм не является адаптивным**: пусть объем данных равен $n$ , числа записаны в системе счисления с основанием $d$ , а максимальное число разрядов равно $k$ . Тогда выполнение сортировки подсчетом для одного разряда требует $O(n + d)$ времени, а сортировка по всем $k$ разрядам требует $O((n + d)k)$ времени. Обычно $d$ и $k$ сравнительно малы, поэтому временная сложность стремится к $O(n)$ .
- **Пространственная сложность равна $O(n + d)$, сортировка не выполняется на месте**: как и в сортировке подсчетом, здесь требуются массивы `res` и `counter` длины $n$ и $d$ .
- **Стабильная сортировка**: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна; если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

+58
View File
@@ -0,0 +1,58 @@
# Сортировка выбором
<u>Сортировка выбором (selection sort)</u> работает очень просто: запускается цикл, и на каждом шаге из неотсортированного диапазона выбирается минимальный элемент, после чего он переносится в конец уже отсортированного диапазона.
Пусть длина массива равна $n$ ; тогда процесс сортировки выбором выглядит так, как показано на рисунке ниже.
1. В начальном состоянии все элементы не отсортированы, то есть неотсортированный диапазон индексов равен $[0, n-1]$ .
2. Выбрать минимальный элемент из диапазона $[0, n-1]$ и поменять его местами с элементом в позиции $0$ . После этого первые 1 элементов массива отсортированы.
3. Выбрать минимальный элемент из диапазона $[1, n-1]$ и поменять его местами с элементом в позиции $1$ . После этого первые 2 элементов массива отсортированы.
4. Продолжать по аналогии. После $n - 1$ раундов выбора и обмена первые $n - 1$ элементов массива будут отсортированы.
5. Оставшийся элемент обязательно является максимальным, сортировать его не нужно, поэтому массив считается отсортированным.
=== "<1>"
![Шаги сортировки выбором](selection_sort.assets/selection_sort_step1.png)
=== "<2>"
![selection_sort_step2](selection_sort.assets/selection_sort_step2.png)
=== "<3>"
![selection_sort_step3](selection_sort.assets/selection_sort_step3.png)
=== "<4>"
![selection_sort_step4](selection_sort.assets/selection_sort_step4.png)
=== "<5>"
![selection_sort_step5](selection_sort.assets/selection_sort_step5.png)
=== "<6>"
![selection_sort_step6](selection_sort.assets/selection_sort_step6.png)
=== "<7>"
![selection_sort_step7](selection_sort.assets/selection_sort_step7.png)
=== "<8>"
![selection_sort_step8](selection_sort.assets/selection_sort_step8.png)
=== "<9>"
![selection_sort_step9](selection_sort.assets/selection_sort_step9.png)
=== "<10>"
![selection_sort_step10](selection_sort.assets/selection_sort_step10.png)
=== "<11>"
![selection_sort_step11](selection_sort.assets/selection_sort_step11.png)
В коде мы используем $k$ для записи минимального элемента в пределах неотсортированного диапазона:
```src
[file]{selection_sort}-[class]{}-[func]{selection_sort}
```
## Характеристики алгоритма
- **Временная сложность равна $O(n^2)$, сортировка не является адаптивной**: внешний цикл выполняется $n - 1$ раз; в первом раунде длина неотсортированного диапазона равна $n$ , а в последнем - $2$ , то есть отдельные раунды содержат $n$, $n - 1$, $\dots$, $3$, $2$ проходов внутреннего цикла, их сумма равна $\frac{(n - 1)(n + 2)}{2}$ .
- **Пространственная сложность равна $O(1)$, сортировка выполняется на месте**: указатели $i$ и $j$ используют константный объем дополнительной памяти.
- **Нестабильная сортировка**: как показано на рисунке ниже, элемент `nums[i]` может быть переставлен вправо от другого равного ему элемента, из-за чего их относительный порядок изменится.
![Пример нестабильности сортировки выбором](selection_sort.assets/selection_sort_instability.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,46 @@
# Алгоритмы сортировки
<u>Алгоритмы сортировки (sorting algorithm)</u> используются для упорядочивания набора данных по определенному правилу. Они применяются очень широко, потому что упорядоченные данные обычно проще и быстрее искать, анализировать и обрабатывать.
Как показано на рисунке ниже, данными в алгоритмах сортировки могут быть целые числа, числа с плавающей запятой, символы, строки и другие типы. Критерий сравнения тоже можно задать по-разному, например по величине чисел, по порядку ASCII-кодов символов или по пользовательскому правилу.
![Примеры типов данных и правил сравнения](sorting_algorithm.assets/sorting_examples.png)
## Критерии оценки
**Скорость выполнения**: мы ожидаем, что временная сложность алгоритма сортировки будет как можно ниже, а общее число операций будет как можно меньше (то есть константа во временной сложности будет небольшой). Для больших объемов данных этот критерий особенно важен.
**Сортировка на месте**: как следует из названия, <u>сортировка на месте</u> выполняется прямо в исходном массиве и не требует дополнительного вспомогательного массива, что позволяет экономить память. Обычно при сортировке на месте переносов данных меньше, а скорость работы выше.
**Стабильность**: <u>стабильная сортировка</u> после завершения не меняет относительный порядок одинаковых элементов в массиве.
Стабильность является необходимым условием для многоуровневой сортировки. Предположим, у нас есть таблица со сведениями о студентах, где в первом и втором столбцах записаны имя и возраст. В этом случае <u>нестабильная сортировка</u> может разрушить уже существующий порядок входных данных:
```shell
# Входные данные уже отсортированы по имени
# (name, age)
('A', 19)
('B', 18)
('C', 21)
('D', 19)
('E', 23)
# Если затем нестабильным алгоритмом отсортировать список по возрасту,
# относительный порядок ('D', 19) и ('A', 19) изменится,
# и свойство упорядоченности по имени будет потеряно
('B', 18)
('D', 19)
('A', 19)
('C', 21)
('E', 23)
```
**Адаптивность**: <u>адаптивная сортировка</u> умеет использовать уже существующий порядок входных данных, чтобы сократить вычисления и добиться лучшей эффективности. Лучшая временная сложность адаптивных алгоритмов обычно лучше их средней временной сложности.
**Основанность на сравнении**: <u>сортировка на основе сравнений</u> использует операторы сравнения ($<$, $=$, $>$), чтобы определить относительный порядок элементов и отсортировать массив; ее теоретически лучшая временная сложность равна $O(n \log n)$ . А вот <u>сортировка без сравнений</u> не опирается на операторы сравнения, поэтому может достигать $O(n)$ , но универсальность у нее ниже.
## Идеальный алгоритм сортировки
**Быстрый, выполняющийся на месте, стабильный, адаптивный и универсальный**. Очевидно, что на сегодняшний день не существует алгоритма сортировки, который одновременно обладал бы всеми этими свойствами. Поэтому при выборе алгоритма сортировки нужно исходить из конкретных особенностей данных и требований задачи.
Далее мы последовательно изучим разные алгоритмы сортировки и на основании приведенных выше критериев разберем их преимущества и недостатки.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+47
View File
@@ -0,0 +1,47 @@
# Резюме
### Ключевые выводы
- Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до $O(n)$ .
- Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна $O(n^2)$ , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.
- Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до $O(n^2)$ . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до $O(\log n)$ .
- Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии "разделяй и властвуй". Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна $O(n)$ ; однако при сортировке связного списка пространственную сложность можно оптимизировать до $O(1)$ .
- Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию "разделяй и властвуй" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
- Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
- Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.
- В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, свойством выполнения на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.
- На рисунке ниже сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.
![Сравнение алгоритмов сортировки](summary.assets/sorting_algorithms_comparison.png)
### Вопросы и ответы
**В**: В каких случаях стабильность алгоритма сортировки является обязательной?
В реальных задачах нам может понадобиться сортировать объекты по некоторому атрибуту. Например, у студентов есть два атрибута: имя и рост. Мы хотим выполнить многоуровневую сортировку: сначала отсортировать по имени и получить `(A, 180) (B, 185) (C, 170) (D, 170)` , а затем отсортировать по росту. Если используемый алгоритм сортировки нестабилен, то мы можем получить `(D, 170) (C, 170) (A, 180) (B, 185)` .
Нетрудно увидеть, что в этом случае студенты D и C поменялись местами, порядок по имени разрушился, а именно этого мы и не хотим.
**В**: Можно ли поменять местами порядок "поиска справа налево" и "поиска слева направо" в разделении с опорным элементом?
Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять "поиск справа налево", а уже затем - "поиск слева направо". Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.
Последний шаг `partition()` - это обмен `nums[left]` и `nums[i]` . После обмена все элементы слева от опорного должны быть `<=` опорного, **а значит, перед этим обменом должно выполняться условие `nums[left] >= nums[i]`**. Если сначала выполнять "поиск слева направо", то в случае, когда не удается найти элемент больше опорного, **цикл завершится в состоянии `i == j` , и при этом может оказаться, что `nums[j] == nums[i] > nums[left]`**. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.
Например, для массива `[0, 0, 0, 0, 1]` , если сначала выполнять "поиск слева направо", после разделения получится `[1, 0, 0, 0, 0]` , а это неправильный результат.
Если же выбрать `nums[right]` в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять "поиск слева направо".
**В**: Почему при оптимизации глубины рекурсии в быстрой сортировке выбор короткого массива гарантирует, что глубина рекурсии не превысит $\log n$ ?
Глубина рекурсии - это число текущих рекурсивных вызовов, которые еще не завершились. На каждом раунде разделения исходный массив разбивается на два подмассива. После оптимизации глубины рекурсии длина подмассива, в который мы продолжаем рекурсивный спуск, не превышает половины длины исходного массива. Если рассматривать худший случай, когда длина каждый раз становится ровно вдвое меньше, итоговая глубина рекурсии и будет равна $\log n$ .
В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов; в худшем случае это будут длины $n$ , $n - 1$ , $\dots$ , $2$ , $1$ , а глубина рекурсии окажется равной $n$ . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.
**В**: Если все элементы массива равны, будет ли временная сложность быстрой сортировки равна $O(n^2)$ ? Как справиться с таким вырождением?
Да. Для этого случая можно рассмотреть разделение массива на три части: элементы меньше опорного, равные опорному и большие опорного. Рекурсию нужно продолжать только для частей меньше и больше опорного. При таком подходе массив, целиком состоящий из одинаковых элементов, будет отсортирован всего за один раунд разделения.
**В**: Почему худшая временная сложность блочной сортировки равна $O(n^2)$ ?
В худшем случае все элементы попадут в один и тот же блок. Если затем сортировать этот блок алгоритмом со сложностью $O(n^2)$ , то общая временная сложность тоже станет $O(n^2)$ .