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
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,59 @@
|
||||
# Сортировка пузырьком
|
||||
|
||||
<u>Сортировка пузырьком (bubble sort)</u> сортирует массив за счет непрерывного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма.
|
||||
|
||||
Как показано на рисунке ниже, процесс "всплытия" можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если "левый элемент > правый элемент", меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
## Алгоритм
|
||||
|
||||
Пусть длина массива равна $n$ ; тогда шаги сортировки пузырьком показаны на рисунке ниже.
|
||||
|
||||
1. Сначала выполнить один проход "всплытия" по $n$ элементам, **переместив максимальный элемент массива на правильную позицию**.
|
||||
2. Затем выполнить "всплытие" по оставшимся $n - 1$ элементам, **переместив второй по величине элемент на правильную позицию**.
|
||||
3. Продолжать по аналогии; после $n - 1$ раундов "всплытия" **первые $n - 1$ по величине элементы окажутся на правильных позициях**.
|
||||
4. Оставшийся единственный элемент обязательно является минимальным, сортировать его уже не нужно, поэтому сортировка завершена.
|
||||
|
||||

|
||||
|
||||
Пример кода:
|
||||
|
||||
```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$ используют константный объем дополнительной памяти.
|
||||
- **Стабильная сортировка**: поскольку при "всплытии" равные элементы не обмениваются местами.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,45 @@
|
||||
# Блочная сортировка
|
||||
|
||||
Рассмотренные выше алгоритмы сортировки относятся к "сортировкам на основе сравнений": они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше $O(n \log n)$ . Далее мы рассмотрим несколько "сортировок без сравнений", чья временная сложность может достигать линейного порядка.
|
||||
|
||||
<u>Блочная сортировка (bucket sort)</u> является типичным применением стратегии "разделяй и властвуй". Она задает несколько упорядоченных по диапазонам блоков, каждый блок соответствует некоторому диапазону значений; затем данные равномерно распределяются по блокам, внутри каждого блока выполняется сортировка, а в конце результаты блоков объединяются по порядку.
|
||||
|
||||
## Алгоритм
|
||||
|
||||
Рассмотрим массив длины $n$, элементы которого являются числами с плавающей запятой из диапазона $[0, 1)$ . Процесс блочной сортировки показан на рисунке ниже.
|
||||
|
||||
1. Инициализировать $k$ блоков и распределить $n$ элементов по этим $k$ блокам.
|
||||
2. Отсортировать каждый блок по отдельности (здесь используется встроенная функция сортировки языка программирования).
|
||||
3. Объединить результаты в порядке следования блоков от меньшего к большему.
|
||||
|
||||

|
||||
|
||||
Код приведен ниже:
|
||||
|
||||
```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 блока; конкретную схему разбиения можно выбирать в зависимости от свойств данных.
|
||||
|
||||

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

|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -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` в порядке возрастания.
|
||||
|
||||

|
||||
|
||||
Код приведен ниже:
|
||||
|
||||
```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>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
Код реализации сортировки подсчетом приведен ниже:
|
||||
|
||||
```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)$ .
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -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>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
=== "<12>"
|
||||

|
||||
|
||||
В реализации кода используется та же функция просеивания сверху вниз `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)$ памяти. Обмен элементов и операции просеивания выполняются прямо в исходном массиве.
|
||||
- **Нестабильная сортировка**: при обмене вершины кучи и нижнего элемента относительный порядок равных элементов может измениться.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Сортировка
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Сортировка упорядочивает хаотичные данные и позволяет быстрее находить закономерности.
|
||||
|
||||
За кажущейся простотой скрывается целая группа алгоритмов с разными достоинствами и ограничениями.
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,46 @@
|
||||
# Сортировка вставками
|
||||
|
||||
<u>Сортировка вставками (insertion sort)</u> - это простой алгоритм сортировки, принцип которого очень похож на ручное упорядочивание колоды карт.
|
||||
|
||||
Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он поочередно сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию.
|
||||
|
||||
На рисунке ниже показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как `base` ; нам нужно сдвинуть все элементы от целевого индекса до `base` на одну позицию вправо, а затем присвоить `base` значение в целевом индексе.
|
||||
|
||||

|
||||
|
||||
## Алгоритм
|
||||
|
||||
Общий процесс сортировки вставками показан на рисунке ниже.
|
||||
|
||||
1. В начальном состоянии отсортирован только первый элемент массива.
|
||||
2. Выбрать второй элемент массива как `base` ; после вставки в правильную позицию **первые 2 элемента массива окажутся отсортированными**.
|
||||
3. Выбрать третий элемент как `base` ; после вставки в правильную позицию **первые 3 элемента массива окажутся отсортированными**.
|
||||
4. Продолжать по аналогии; в последнем раунде в качестве `base` берется последний элемент, и после его вставки **все элементы массива будут отсортированы**.
|
||||
|
||||

|
||||
|
||||
Пример кода:
|
||||
|
||||
```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)$ . **Если входные данные уже частично упорядочены, сортировка вставками обычно эффективнее сортировки выбором**.
|
||||
- Сортировка выбором нестабильна, поэтому ее нельзя использовать для многоуровневой сортировки.
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
@@ -0,0 +1,73 @@
|
||||
# Сортировка слиянием
|
||||
|
||||
<u>Сортировка слиянием (merge sort)</u> - это алгоритм сортировки на основе стратегии "разделяй и властвуй", который включает этапы "разделения" и "слияния", показанные на рисунке ниже.
|
||||
|
||||
1. **Этап разделения**: массив рекурсивно разбивается от середины, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов.
|
||||
2. **Этап слияния**: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; левые и правые короткие упорядоченные массивы непрерывно объединяются в более длинный упорядоченный массив, пока процесс не завершится.
|
||||
|
||||

|
||||
|
||||
## Алгоритм
|
||||
|
||||
Как показано на рисунке ниже, на этапе "разделения" массив рекурсивно разбивается сверху вниз по середине на два подмассива.
|
||||
|
||||
1. Вычислить середину массива `mid` и рекурсивно разделить левый подмассив (интервал `[left, mid]` ) и правый подмассив (интервал `[mid + 1, right]` ).
|
||||
2. Рекурсивно повторять шаг `1.` , пока длина подмассива не станет равной 1.
|
||||
|
||||
Этап "слияния" снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
Нетрудно заметить, что порядок рекурсии в сортировке слиянием совпадает с порядком рекурсии при постфиксном обходе бинарного дерева.
|
||||
|
||||
- **Постфиксный обход**: сначала рекурсивно обходится левое поддерево, затем правое поддерево, а в конце обрабатывается корневой узел.
|
||||
- **Сортировка слиянием**: сначала рекурсивно обрабатывается левый подмассив, затем правый подмассив, а в конце выполняется слияние.
|
||||
|
||||
Реализация сортировки слиянием показана в коде ниже. Обратите внимание: в `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)$**.
|
||||
|
||||
- **Этап разделения**: работу по разбиению списка можно реализовать с помощью "итерации" вместо "рекурсии", тем самым устранив расход памяти на стек вызовов.
|
||||
- **Этап слияния**: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список.
|
||||
|
||||
Детали реализации достаточно сложны; заинтересованные читатели могут изучить соответствующие материалы самостоятельно.
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,100 @@
|
||||
# Быстрая сортировка
|
||||
|
||||
<u>Быстрая сортировка (quick sort)</u> - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй"; он работает эффективно и применяется очень широко.
|
||||
|
||||
Ключевая операция быстрой сортировки - это "разделение с опорным элементом". Ее цель такова: выбрать некоторый элемент массива в качестве "опорного" и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке ниже.
|
||||
|
||||
1. Выбрать самый левый элемент массива как опорный и инициализировать два указателя `i` и `j` , направленные на левую и правую границы массива.
|
||||
2. Запустить цикл, в котором `i` и `j` ищут соответственно первый элемент, больший опорного, и первый элемент, меньший опорного, после чего эти два элемента меняются местами.
|
||||
3. Повторять шаг `2.` , пока указатели `i` и `j` не встретятся, а затем обменять опорный элемент с элементом на границе двух подмассивов.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив; при этом выполняется условие "любой элемент левого подмассива $\leq$ опорный элемент $\leq$ любой элемент правого подмассива". Следовательно, далее нам нужно лишь отсортировать эти два подмассива.
|
||||
|
||||
!!! note "Стратегия divide and conquer в быстрой сортировке"
|
||||
|
||||
По сути, разделение с опорным элементом сводит задачу сортировки длинного массива к двум задачам сортировки более коротких массивов.
|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort}-[func]{partition}
|
||||
```
|
||||
|
||||
## Алгоритм
|
||||
|
||||
Общий процесс быстрой сортировки показан на рисунке ниже.
|
||||
|
||||
1. Сначала выполнить "разделение с опорным элементом" для исходного массива и получить неотсортированные левый и правый подмассивы.
|
||||
2. Затем рекурсивно выполнить "разделение с опорным элементом" для левого и правого подмассивов.
|
||||
3. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1; после этого сортировка всего массива будет завершена.
|
||||
|
||||

|
||||
|
||||
```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}
|
||||
```
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -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.` , продолжая процесс, пока сортировка не будет выполнена для всех разрядов.
|
||||
|
||||

|
||||
|
||||
Ниже разберем реализацию кода. Для числа $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$ .
|
||||
- **Стабильная сортировка**: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна; если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -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>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
В коде мы используем $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]` может быть переставлен вправо от другого равного ему элемента, из-за чего их относительный порядок изменится.
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,46 @@
|
||||
# Алгоритмы сортировки
|
||||
|
||||
<u>Алгоритмы сортировки (sorting algorithm)</u> используются для упорядочивания набора данных по определенному правилу. Они применяются очень широко, потому что упорядоченные данные обычно проще и быстрее искать, анализировать и обрабатывать.
|
||||
|
||||
Как показано на рисунке ниже, данными в алгоритмах сортировки могут быть целые числа, числа с плавающей запятой, символы, строки и другие типы. Критерий сравнения тоже можно задать по-разному, например по величине чисел, по порядку ASCII-кодов символов или по пользовательскому правилу.
|
||||
|
||||

|
||||
|
||||
## Критерии оценки
|
||||
|
||||
**Скорость выполнения**: мы ожидаем, что временная сложность алгоритма сортировки будет как можно ниже, а общее число операций будет как можно меньше (то есть константа во временной сложности будет небольшой). Для больших объемов данных этот критерий особенно важен.
|
||||
|
||||
**Сортировка на месте**: как следует из названия, <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)$ , но универсальность у нее ниже.
|
||||
|
||||
## Идеальный алгоритм сортировки
|
||||
|
||||
**Быстрый, выполняющийся на месте, стабильный, адаптивный и универсальный**. Очевидно, что на сегодняшний день не существует алгоритма сортировки, который одновременно обладал бы всеми этими свойствами. Поэтому при выборе алгоритма сортировки нужно исходить из конкретных особенностей данных и требований задачи.
|
||||
|
||||
Далее мы последовательно изучим разные алгоритмы сортировки и на основании приведенных выше критериев разберем их преимущества и недостатки.
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,47 @@
|
||||
# Резюме
|
||||
|
||||
### Ключевые выводы
|
||||
|
||||
- Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до $O(n)$ .
|
||||
- Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна $O(n^2)$ , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.
|
||||
- Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до $O(n^2)$ . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до $O(\log n)$ .
|
||||
- Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии "разделяй и властвуй". Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна $O(n)$ ; однако при сортировке связного списка пространственную сложность можно оптимизировать до $O(1)$ .
|
||||
- Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию "разделяй и властвуй" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
|
||||
- Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
|
||||
- Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.
|
||||
- В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, свойством выполнения на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.
|
||||
- На рисунке ниже сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.
|
||||
|
||||

|
||||
|
||||
### Вопросы и ответы
|
||||
|
||||
**В**: В каких случаях стабильность алгоритма сортировки является обязательной?
|
||||
|
||||
В реальных задачах нам может понадобиться сортировать объекты по некоторому атрибуту. Например, у студентов есть два атрибута: имя и рост. Мы хотим выполнить многоуровневую сортировку: сначала отсортировать по имени и получить `(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)$ .
|
||||