This commit is contained in:
krahets
2026-04-14 18:06:14 +08:00
parent 065a978848
commit e53a7f2498
93 changed files with 565 additions and 570 deletions
+9 -9
View File
@@ -6,7 +6,7 @@ comments: true
<u>Сортировка пузырьком (bubble sort)</u> реализует сортировку путем последовательного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма.
Как показано на рисунке 11-4, процесс "всплытия" можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если "левый элемент > правый элемент", меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.
Как показано на рисунке 11-4, процесс «всплытия» можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если «левый элемент > правый элемент», меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.
=== "<1>"
![Моделирование пузырька через обмен элементов](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" }
@@ -33,11 +33,11 @@ comments: true
## 11.3.1 &nbsp; Алгоритм
Пусть длина массива равна $n$ ; тогда шаги сортировки пузырьком показаны на рисунке 11-5.
Пусть длина массива равна $n$. Тогда шаги сортировки пузырьком показаны на рисунке 11-5.
1. Сначала выполнить один проход "всплытия" по $n$ элементам, **переместив максимальный элемент массива на правильную позицию**.
2. Затем выполнить "всплытие" по оставшимся $n - 1$ элементам, **переместив второй по величине элемент на правильную позицию**.
3. Продолжать по аналогии; после $n - 1$ раундов "всплытия" **первые $n - 1$ по величине элементы окажутся на правильных позициях**.
1. Сначала выполнить один проход «всплытия» по $n$ элементам, **переместив максимальный элемент массива на правильную позицию**.
2. Затем выполнить «всплытие» по оставшимся $n - 1$ элементам, **переместив второй по величине элемент на правильную позицию**.
3. Продолжать по аналогии. После $n - 1$ раундов «всплытия» **первые $n - 1$ по величине элементы окажутся на правильных позициях**.
4. Оставшийся единственный элемент обязательно является минимальным, сортировать его уже не нужно, поэтому сортировка завершена.
![Процесс сортировки пузырьком](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" }
@@ -297,9 +297,9 @@ comments: true
## 11.3.2 &nbsp; Оптимизация эффективности
Если в каком-либо раунде "всплытия" не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг `flag` для отслеживания этой ситуации и немедленного выхода.
Если в каком-либо раунде «всплытия» не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг `flag` для отслеживания этой ситуации и немедленного выхода.
После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны $O(n^2)$ ; однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность $O(n)$ .
После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны $O(n^2)$. Однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность $O(n)$ .
=== "Python"
@@ -617,6 +617,6 @@ comments: true
## 11.3.3 &nbsp; Характеристики алгоритма
- **Временная сложность равна $O(n^2)$, алгоритм адаптивен**: длины диапазонов, проходящих "всплытие" в разных раундах, последовательно равны $n - 1$, $n - 2$, $\dots$, $2$, $1$ , а их сумма равна $(n - 1) n / 2$ . После добавления оптимизации с `flag` лучшая временная сложность может достигать $O(n)$ .
- **Временная сложность равна $O(n^2)$, алгоритм адаптивен**: длины диапазонов, проходящих «всплытие» в разных раундах, последовательно равны $n - 1$, $n - 2$, $\dots$, $2$, $1$ , а их сумма равна $(n - 1) n / 2$ . После добавления оптимизации с `flag` лучшая временная сложность может достигать $O(n)$ .
- **Пространственная сложность равна $O(1)$, сортировка выполняется на месте**: указатели $i$ и $j$ используют константный объем дополнительной памяти.
- **Стабильная сортировка**: поскольку при "всплытии" равные элементы не обмениваются местами.
- **Стабильная сортировка**: поскольку при «всплытии» равные элементы не обмениваются местами.
+4 -4
View File
@@ -4,9 +4,9 @@ comments: true
# 11.8 &nbsp; Блочная сортировка
Рассмотренные выше алгоритмы сортировки относятся к "сортировкам на основе сравнений": они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше $O(n \log n)$ . Далее мы рассмотрим несколько "сортировок без сравнений", чья временная сложность может достигать линейного порядка.
Рассмотренные выше алгоритмы сортировки относятся к «сортировкам на основе сравнений»: они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше $O(n \log n)$ . Далее мы рассмотрим несколько «сортировок без сравнений», чья временная сложность может достигать линейного порядка.
<u>Блочная сортировка (bucket sort)</u> является типичным применением стратегии "разделяй и властвуй". Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных; затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.
<u>Блочная сортировка (bucket sort)</u> является типичным применением стратегии «разделяй и властвуй». Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных. Затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.
## 11.8.1 &nbsp; Алгоритм
@@ -452,11 +452,11 @@ comments: true
## 11.8.3 &nbsp; Как добиться равномерного распределения
Теоретически временная сложность блочной сортировки может достигать $O(n)$ ; **ключ к этому - как можно более равномерно распределить элементы по блокам**. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.
Теоретически временная сложность блочной сортировки может достигать $O(n)$. **Ключ к этому - как можно более равномерно распределить элементы по блокам**. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.
Чтобы добиться более равномерного распределения, можно сначала задать грубую линию раздела и приблизительно распределить данные по 3 блокам. **После этого блоки с большим числом товаров можно снова делить на 3 блока и продолжать процесс до тех пор, пока число элементов в каждом блоке не станет примерно одинаковым**.
Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока; конкретную схему разбиения можно выбирать в зависимости от свойств данных.
Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока. Конкретную схему разбиения можно выбирать в зависимости от свойств данных.
![Рекурсивное разбиение по блокам](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" }
+5 -5
View File
@@ -8,10 +8,10 @@ comments: true
## 11.9.1 &nbsp; Простая реализация
Сначала рассмотрим простой пример. Дан массив `nums` длины $n$ , элементы которого являются "неотрицательными целыми числами". Общий процесс сортировки подсчетом показан на рисунке 11-16.
Сначала рассмотрим простой пример. Дан массив `nums` длины $n$ , элементы которого являются «неотрицательными целыми числами». Общий процесс сортировки подсчетом показан на рисунке 11-16.
1. Пройти по массиву, найти в нем максимальное число, обозначить его как $m$ , а затем создать вспомогательный массив `counter` длины $m + 1$ .
2. **С помощью `counter` подсчитать, сколько раз каждое число встречается в `nums`**; при этом `counter[num]` хранит число вхождений значения `num` . Делается это просто: достаточно пройти по `nums` (пусть текущее число равно `num` ) и на каждом шаге увеличить `counter[num]` на $1$ .
2. **С помощью `counter` подсчитать, сколько раз каждое число встречается в `nums`**. При этом `counter[num]` хранит число вхождений значения `num` . Делается это просто: достаточно пройти по `nums` (пусть текущее число равно `num` ) и на каждом шаге увеличить `counter[num]` на $1$ .
3. **Поскольку индексы массива `counter` изначально упорядочены, можно считать, что все числа уже отсортированы**. Далее остается пройти по `counter` и в соответствии с числом вхождений записать значения обратно в `nums` в порядке возрастания.
![Процесс сортировки подсчетом](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" }
@@ -374,7 +374,7 @@ comments: true
Внимательный читатель мог заметить, что **если входные данные представлены объектами, то описанный выше шаг `3.` перестает работать**. Например, если входными данными являются объекты товаров и мы хотим отсортировать их по цене (полю класса), то описанный алгоритм сможет выдать только отсортированный ряд цен, но не исходные объекты в нужном порядке.
Как же получить корректный порядок исходных данных? Сначала вычислим "префиксную сумму" массива `counter` . Как следует из названия, префиксная сумма в индексе `i` , обозначаемая как `prefix[i]` , равна сумме первых `i` элементов массива:
Как же получить корректный порядок исходных данных? Сначала вычислим «префиксную сумму» массива `counter` . Как следует из названия, префиксная сумма в индексе `i` , обозначаемая как `prefix[i]` , равна сумме первых `i` элементов массива:
$$
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
@@ -385,7 +385,7 @@ $$
1. Записать `num` в массив `res` по индексу `prefix[num] - 1` .
2. Уменьшить префиксную сумму `prefix[num]` на $1$ , чтобы получить индекс следующего размещения элемента `num` .
После завершения прохода массив `res` будет содержать отсортированный результат; остается только переписать `res` обратно в `nums` . Полный процесс сортировки подсчетом показан на рисунке 11-17.
После завершения прохода массив `res` будет содержать отсортированный результат. Остается только переписать `res` обратно в `nums` . Полный процесс сортировки подсчетом показан на рисунке 11-17.
=== "<1>"
![Шаги сортировки подсчетом](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
@@ -886,7 +886,7 @@ $$
- **Временная сложность равна $O(n + m)$, алгоритм не является адаптивным** : необходимо пройти по `nums` и по `counter` , а оба этих прохода занимают линейное время. Обычно выполняется $n \gg m$ , поэтому временная сложность стремится к $O(n)$ .
- **Пространственная сложность равна $O(n + m)$, сортировка не выполняется на месте**: используются массивы `res` и `counter` длины $n$ и $m$ соответственно.
- **Стабильная сортировка**: порядок заполнения `res` идет "справа налево", поэтому обратный проход по `nums` позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по `nums` тоже даст правильный результат сортировки, но он будет нестабильным.
- **Стабильная сортировка**: порядок заполнения `res` идет «справа налево», поэтому обратный проход по `nums` позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по `nums` тоже даст правильный результат сортировки, но он будет нестабильным.
## 11.9.4 &nbsp; Ограничения
+5 -5
View File
@@ -6,18 +6,18 @@ comments: true
!!! tip
Перед чтением этого раздела убедитесь, что вы уже изучили главу "Куча".
Перед чтением этого раздела убедитесь, что вы уже изучили главу «Куча».
<u>Пирамидальная сортировка (heap sort)</u> - это эффективный алгоритм сортировки, основанный на структуре данных "куча". Для его реализации можно использовать уже изученные нами "построение кучи" и "извлечение элементов из кучи".
<u>Пирамидальная сортировка (heap sort)</u> - это эффективный алгоритм сортировки, основанный на структуре данных «куча». Для его реализации можно использовать уже изученные нами «построение кучи» и «извлечение элементов из кучи».
1. Подать на вход массив и построить из него мин-кучу; в этот момент минимальный элемент будет находиться в вершине кучи.
1. Подать на вход массив и построить из него мин-кучу. В этот момент минимальный элемент будет находиться в вершине кучи.
2. Непрерывно выполнять извлечение из кучи и по порядку записывать извлеченные элементы - так получится последовательность, отсортированная по возрастанию.
Хотя этот метод и работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию.
## 11.7.1 &nbsp; Алгоритм
Пусть длина массива равна $n$ ; тогда процесс пирамидальной сортировки показан на рисунке 11-12.
Пусть длина массива равна $n$. Тогда процесс пирамидальной сортировки показан на рисунке 11-12.
1. Подать на вход массив и построить из него макс-кучу. После этого максимальный элемент окажется в вершине кучи.
2. Обменять элемент в вершине кучи (первый элемент) с элементом внизу кучи (последний элемент). После обмена длина кучи уменьшается на $1$ , а число уже отсортированных элементов увеличивается на $1$ .
@@ -66,7 +66,7 @@ comments: true
<p align="center"> Рисунок 11-12 &nbsp; Шаги пирамидальной сортировки </p>
В коде используется та же функция просеивания сверху вниз `sift_down()`, что и в главе "Куча". Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции `sift_down()` нужно передавать параметр длины $n$ , чтобы указать текущую действительную длину кучи. Код приведен ниже:
В коде используется та же функция просеивания сверху вниз `sift_down()`, что и в главе «Куча». Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции `sift_down()` нужно передавать параметр длины $n$ , чтобы указать текущую действительную длину кучи. Код приведен ниже:
=== "Python"
+1 -1
View File
@@ -10,7 +10,7 @@ icon: material/sort-ascending
!!! abstract
Сортировка упорядочивает хаотичные данные и позволяет быстрее находить закономерности.
За кажущейся простотой скрывается целая группа алгоритмов с разными достоинствами и ограничениями.
## Содержание главы
+7 -7
View File
@@ -8,7 +8,7 @@ comments: true
Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию.
На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как `base` ; нам нужно сдвинуть все элементы от целевого индекса до `base` на одну позицию вправо, а затем записать `base` в целевой индекс.
На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как `base`. Нам нужно сдвинуть все элементы от целевого индекса до `base` на одну позицию вправо, а затем записать `base` в целевой индекс.
![Одна операция вставки](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }
@@ -19,9 +19,9 @@ comments: true
Общий процесс сортировки вставками показан на рисунке 11-7.
1. В начальном состоянии отсортирован только первый элемент массива.
2. Выбрать второй элемент массива как `base` ; после вставки в правильную позицию **первые два элемента массива окажутся отсортированными**.
3. Выбрать третий элемент как `base` ; после вставки в правильную позицию **первые три элемента массива окажутся отсортированными**.
4. Продолжать по аналогии; в последнем раунде в качестве `base` берется последний элемент, и после его вставки **все элементы массива будут отсортированы**.
2. Выбрать второй элемент массива как `base`. После вставки в правильную позицию **первые два элемента массива окажутся отсортированными**.
3. Выбрать третий элемент как `base`. После вставки в правильную позицию **первые три элемента массива окажутся отсортированными**.
4. Продолжать по аналогии. В последнем раунде в качестве `base` берется последний элемент, и после его вставки **все элементы массива будут отсортированы**.
![Процесс сортировки вставками](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" }
@@ -285,12 +285,12 @@ comments: true
Временная сложность сортировки вставками равна $O(n^2)$ , а у быстрой сортировки, которую мы скоро изучим, временная сложность равна $O(n \log n)$ . Несмотря на более высокую асимптотическую сложность, **на малых объемах данных сортировка вставками обычно работает быстрее**.
Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня $O(n \log n)$ , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии "разделяй и властвуй" и обычно включают больше элементарных вычислений. Когда объем данных мал, значения $n^2$ и $n \log n$ близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.
Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня $O(n \log n)$ , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии «разделяй и властвуй» и обычно включают больше элементарных вычислений. Когда объем данных мал, значения $n^2$ и $n \log n$ близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.
На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии "разделяй и властвуй", например быструю сортировку; для коротких массивов сразу использовать сортировку вставками.
На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии «разделяй и властвуй», например быструю сортировку. Для коротких массивов сразу использовать сортировку вставками.
Хотя сортировка пузырьком, выбором и вставками имеют одинаковую временную сложность $O(n^2)$ , в реальных задачах **сортировка вставками используется заметно чаще, чем сортировка пузырьком и сортировка выбором**. Основные причины таковы.
- Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции; сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому **вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками**.
- Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции. Сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому **вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками**.
- Временная сложность сортировки выбором в любом случае равна $O(n^2)$ . **Если входные данные уже частично упорядочены, сортировка вставками обычно эффективнее сортировки выбором**.
- Сортировка выбором нестабильна, поэтому ее нельзя использовать для многоуровневой сортировки.
+6 -6
View File
@@ -4,10 +4,10 @@ comments: true
# 11.6 &nbsp; Сортировка слиянием
<u>Сортировка слиянием (merge sort)</u> - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй", который включает этапы "разделения" и "слияния", показанные на рисунке 11-10.
<u>Сортировка слиянием (merge sort)</u> - это алгоритм сортировки, основанный на стратегии «разделяй и властвуй», который включает этапы «разделения» и «слияния», показанные на рисунке 11-10.
1. **Этап разделения**: массив рекурсивно делится пополам, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов.
2. **Этап слияния**: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.
2. **Этап слияния**: когда длина подмассива становится равной 1, разделение завершается и начинается слияние. Два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.
![Этапы разделения и слияния в сортировке слиянием](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" }
@@ -15,12 +15,12 @@ comments: true
## 11.6.1 &nbsp; Алгоритм
Как показано на рисунке 11-11, на этапе "разделения" массив рекурсивно разбивается сверху вниз по середине на два подмассива.
Как показано на рисунке 11-11, на этапе «разделения» массив рекурсивно разбивается сверху вниз по середине на два подмассива.
1. Вычислить середину массива `mid` и рекурсивно разделить левый подмассив (интервал `[left, mid]` ) и правый подмассив (интервал `[mid + 1, right]` ).
2. Рекурсивно повторять шаг `1.` , пока длина подмассива не станет равной 1.
Этап "слияния" снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.
Этап «слияния» снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.
=== "<1>"
![Шаги сортировки слиянием](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" }
@@ -694,7 +694,7 @@ comments: true
Для связных списков сортировка слиянием имеет заметное преимущество перед другими алгоритмами сортировки: **пространственную сложность задачи сортировки списка можно оптимизировать до $O(1)$**.
- **Этап разделения**: работу по разбиению списка можно реализовать с помощью "итерации" вместо "рекурсии", тем самым устранив расход памяти на стек вызовов.
- **Этап разделения**: работу по разбиению списка можно реализовать с помощью «итерации» вместо «рекурсии», тем самым устранив расход памяти на стек вызовов.
- **Этап слияния**: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список.
Детали реализации достаточно сложны; заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.
Детали реализации достаточно сложны. Заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.
+13 -13
View File
@@ -4,9 +4,9 @@ comments: true
# 11.5 &nbsp; Быстрая сортировка
<u>Быстрая сортировка (quick sort)</u> - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй"; он работает эффективно и применяется очень широко.
<u>Быстрая сортировка (quick sort)</u> - это алгоритм сортировки, основанный на стратегии «разделяй и властвуй». Он работает эффективно и применяется очень широко.
Ключевая операция быстрой сортировки - это "разделение с опорным элементом". Ее цель такова: выбрать некоторый элемент массива в качестве "опорного" и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке 11-8.
Ключевая операция быстрой сортировки - это «разделение с опорным элементом». Ее цель такова: выбрать некоторый элемент массива в качестве «опорного» и переместить все элементы меньше опорного влево от него, а все элементы больше опорного - вправо. Конкретный процесс показан на рисунке 11-8.
1. Выбрать самый левый элемент массива как опорный и инициализировать два указателя `i` и `j` , направленные на левую и правую границы массива.
2. Запустить цикл, в котором `i` и `j` ищут соответственно первый элемент, больший опорного, и первый элемент, меньший опорного, после чего эти два элемента меняются местами.
@@ -41,7 +41,7 @@ comments: true
<p align="center"> Рисунок 11-8 &nbsp; Шаги разделения с опорным элементом </p>
После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив; при этом выполняется условие "любой элемент левого подмассива $\leq$ опорный элемент $\leq$ любой элемент правого подмассива". Следовательно, далее нам нужно лишь отсортировать эти два подмассива.
После завершения разделения исходный массив разбивается на три части: левый подмассив, опорный элемент и правый подмассив. При этом выполняется условие «любой элемент левого подмассива $\leq$ опорный элемент $\leq$ любой элемент правого подмассива». Следовательно, далее нам нужно лишь отсортировать эти два подмассива.
!!! note "Стратегия разделяй и властвуй в быстрой сортировке"
@@ -375,9 +375,9 @@ comments: true
Общий процесс быстрой сортировки показан на рисунке 11-9.
1. Сначала выполнить "разделение с опорным элементом" для исходного массива и получить неотсортированные левый и правый подмассивы.
2. Затем рекурсивно выполнить "разделение с опорным элементом" для левого и правого подмассивов.
3. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1; после этого сортировка всего массива будет завершена.
1. Сначала выполнить «разделение с опорным элементом» для исходного массива и получить неотсортированные левый и правый подмассивы.
2. Затем рекурсивно выполнить «разделение с опорным элементом» для левого и правого подмассивов.
3. Продолжать рекурсию до тех пор, пока длина подмассива не станет равной 1. После этого сортировка всего массива будет завершена.
![Процесс быстрой сортировки](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" }
@@ -600,27 +600,27 @@ comments: true
## 11.5.2 &nbsp; Характеристики алгоритма
- **Временная сложность равна $O(n \log n)$, алгоритм не является адаптивным**: в среднем глубина рекурсии при разделении равна $\log n$ , а суммарное число циклов на каждом уровне равно $n$ , поэтому общая сложность составляет $O(n \log n)$ . В худшем случае каждое разделение делит массив длины $n$ на подмассивы длины $0$ и $n - 1$ ; тогда глубина рекурсии достигает $n$ , на каждом уровне выполняется $n$ операций, и общая временная сложность вырождается в $O(n^2)$ .
- **Временная сложность равна $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)$ памяти под стек вызовов. При этом сама сортировка выполняется в исходном массиве без дополнительного массива.
- **Нестабильная сортировка**: на последнем шаге разделения опорный элемент может быть обменян вправо от равного ему элемента.
## 11.5.3 &nbsp; Почему быстрая сортировка быстрая
Уже по названию понятно, что быстрая сортировка должна иметь преимущества по эффективности. Хотя ее средняя временная сложность совпадает со сложностью "сортировки слиянием" и "пирамидальной сортировки", на практике быстрая сортировка обычно работает быстрее. Основные причины таковы.
Уже по названию понятно, что быстрая сортировка должна иметь преимущества по эффективности. Хотя ее средняя временная сложность совпадает со сложностью «сортировки слиянием» и «пирамидальной сортировки», на практике быстрая сортировка обычно работает быстрее. Основные причины таковы.
- **Вероятность худшего случая очень мала**: хотя худшая временная сложность быстрой сортировки равна $O(n^2)$ и она не так стабильна, как сортировка слиянием, в подавляющем большинстве случаев она работает за $O(n \log n)$ .
- **Высокая эффективность использования кэша**: при выполнении разделения система может загрузить весь подмассив в кэш, поэтому доступ к элементам оказывается быстрым. Алгоритмы вроде "пирамидальной сортировки" требуют скачкообразного доступа к элементам и таким свойством не обладают.
- **Небольшой константный множитель в сложности**: среди трех перечисленных алгоритмов у быстрой сортировки обычно меньше всего сравнений, присваиваний и обменов. Это похоже на причину, по которой "сортировка вставками" часто быстрее "сортировки пузырьком".
- **Высокая эффективность использования кэша**: при выполнении разделения система может загрузить весь подмассив в кэш, поэтому доступ к элементам оказывается быстрым. Алгоритмы вроде «пирамидальной сортировки» требуют скачкообразного доступа к элементам и таким свойством не обладают.
- **Небольшой константный множитель в сложности**: среди трех перечисленных алгоритмов у быстрой сортировки обычно меньше всего сравнений, присваиваний и обменов. Это похоже на причину, по которой «сортировка вставками» часто быстрее «сортировки пузырьком».
## 11.5.4 &nbsp; Оптимизация выбора опорного элемента
**На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться**. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет $n - 1$ , а длина правого - $0$ . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину $0$ , стратегия "разделяй и властвуй" потеряет смысл, а быстрая сортировка выродится в нечто близкое к "сортировке пузырьком".
**На некоторых входных данных временная эффективность быстрой сортировки может ухудшаться**. Рассмотрим крайний случай: входной массив полностью отсортирован в обратном порядке. Поскольку в качестве опорного мы выбираем самый левый элемент, после разделения он будет обменян в самый правый конец массива, из-за чего длина левого подмассива станет $n - 1$ , а длина правого - $0$ . Если рекурсия будет продолжаться таким образом, то после каждого разделения один из подмассивов будет иметь длину $0$ , стратегия «разделяй и властвуй» потеряет смысл, а быстрая сортировка выродится в нечто близкое к «сортировке пузырьком».
Чтобы по возможности избежать такого сценария, **можно улучшить стратегию выбора опорного элемента в процедуре разделения**. Например, можно выбирать случайный элемент массива как опорный. Однако если не повезет и каждый раз будет выбираться неудачный опорный элемент, производительность все равно останется неудовлетворительной.
Стоит учитывать, что языки программирования обычно генерируют псевдослучайные числа. Если специально построить тестовый пример под такую последовательность, эффективность быстрой сортировки все равно может деградировать.
Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и **использовать медиану этих трех значений как опорный элемент**. Благодаря этому вероятность того, что опорный элемент окажется "не слишком маленьким и не слишком большим", заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до $O(n^2)$ существенно уменьшается.
Чтобы улучшить ситуацию, можно взять три кандидата (обычно первый, последний и средний элементы массива) и **использовать медиану этих трех значений как опорный элемент**. Благодаря этому вероятность того, что опорный элемент окажется «не слишком маленьким и не слишком большим», заметно возрастает. Конечно, можно брать и большее число кандидатов, чтобы еще сильнее повысить устойчивость алгоритма. После этого вероятность деградации временной сложности до $O(n^2)$ существенно уменьшается.
Пример кода:
@@ -1100,7 +1100,7 @@ comments: true
## 11.5.5 &nbsp; Оптимизация глубины рекурсии
**На некоторых входных данных быстрая сортировка может занимать слишком много памяти**. Рассмотрим полностью отсортированный входной массив. Пусть длина текущего подмассива в рекурсии равна $m$ ; тогда после каждого разделения будут получаться левый подмассив длины $0$ и правый подмассив длины $m - 1$ . Это означает, что на каждом уровне размер задачи уменьшается совсем немного (лишь на один элемент), а высота дерева рекурсии достигает $n - 1$ , поэтому требуется $O(n)$ памяти под стек вызовов.
**На некоторых входных данных быстрая сортировка может занимать слишком много памяти**. Рассмотрим полностью отсортированный входной массив. Пусть длина текущего подмассива в рекурсии равна $m$. Тогда после каждого разделения будут получаться левый подмассив длины $0$ и правый подмассив длины $m - 1$ . Это означает, что на каждом уровне размер задачи уменьшается совсем немного (лишь на один элемент), а высота дерева рекурсии достигает $n - 1$ , поэтому требуется $O(n)$ памяти под стек вызовов.
Чтобы избежать накопления стековых кадров, после каждого разделения можно сравнивать длины двух подмассивов и **рекурсивно обрабатывать только более короткий из них**. Поскольку длина короткого подмассива не превысит $n / 2$ , такой подход гарантирует, что глубина рекурсии не превысит $\log n$ , а худшая пространственная сложность будет оптимизирована до $O(\log n)$ . Код приведен ниже:
+3 -3
View File
@@ -4,7 +4,7 @@ comments: true
# 11.10 &nbsp; Поразрядная сортировка
В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных $n$ велик, а диапазон значений $m$ сравнительно мал. Предположим теперь, что нужно отсортировать $n = 10^6$ номеров студентов, причем каждый номер представляет собой $8$-значное число. Тогда диапазон данных $m = 10^8$ оказывается очень большим; сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.
В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных $n$ велик, а диапазон значений $m$ сравнительно мал. Предположим теперь, что нужно отсортировать $n = 10^6$ номеров студентов, причем каждый номер представляет собой $8$-значное число. Тогда диапазон данных $m = 10^8$ оказывается очень большим. Сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.
<u>Поразрядная сортировка (radix sort)</u> по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. При этом поразрядная сортировка использует соотношение между разрядами числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат.
@@ -13,7 +13,7 @@ comments: true
Рассмотрим пример со студенческими номерами: будем считать, что младший разряд имеет номер $1$ , а старший - номер $8$ . Тогда процесс поразрядной сортировки показан на рисунке 11-18.
1. Инициализировать номер разряда $k = 1$ .
2. Выполнить "сортировку подсчетом" по $k$-му разряду студенческого номера. После этого данные будут упорядочены по $k$-му разряду по возрастанию.
2. Выполнить «сортировку подсчетом» по $k$-му разряду студенческого номера. После этого данные будут упорядочены по $k$-му разряду по возрастанию.
3. Увеличить $k$ на $1$ и вернуться к шагу `2.` , продолжая процесс, пока сортировка не будет выполнена для всех разрядов.
![Процесс поразрядной сортировки](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" }
@@ -737,4 +737,4 @@ $$
- **Временная сложность равна $O(nk)$, алгоритм не является адаптивным**: пусть объем данных равен $n$ , числа записаны в системе счисления с основанием $d$ , а максимальное число разрядов равно $k$ . Тогда выполнение сортировки подсчетом для одного разряда требует $O(n + d)$ времени, а сортировка по всем $k$ разрядам требует $O((n + d)k)$ времени. Обычно $d$ и $k$ сравнительно малы, поэтому временная сложность стремится к $O(n)$ .
- **Пространственная сложность равна $O(n + d)$, сортировка не выполняется на месте**: как и в сортировке подсчетом, здесь требуются массивы `res` и `counter` длины $n$ и $d$ .
- **Стабильная сортировка**: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна; если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
- **Стабильная сортировка**: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна. Если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.
+2 -2
View File
@@ -6,7 +6,7 @@ comments: true
<u>Сортировка выбором (selection sort)</u> работает очень просто: запускается цикл, и на каждом шаге из неотсортированного диапазона выбирается минимальный элемент, после чего он переносится в конец уже отсортированного диапазона.
Пусть длина массива равна $n$ ; тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.
Пусть длина массива равна $n$. Тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.
1. В начальном состоянии все элементы не отсортированы, то есть неотсортированный диапазон индексов равен $[0, n-1]$ .
2. Выбрать минимальный элемент из диапазона $[0, n-1]$ и поменять его местами с элементом в позиции $0$ . После этого первый элемент массива отсортирован.
@@ -331,7 +331,7 @@ comments: true
## 11.2.1 &nbsp; Характеристики алгоритма
- **Временная сложность равна $O(n^2)$, сортировка не является адаптивной**: внешний цикл выполняется $n - 1$ раз; в первом раунде длина неотсортированного диапазона равна $n$ , а в последнем - $2$ , то есть отдельные раунды содержат $n$, $n - 1$, $\dots$, $3$, $2$ проходов внутреннего цикла, их сумма равна $\frac{(n - 1)(n + 2)}{2}$ .
- **Временная сложность равна $O(n^2)$, сортировка не является адаптивной**: внешний цикл выполняется $n - 1$ раз. В первом раунде длина неотсортированного диапазона равна $n$ , а в последнем - $2$ , то есть отдельные раунды содержат $n$, $n - 1$, $\dots$, $3$, $2$ проходов внутреннего цикла, их сумма равна $\frac{(n - 1)(n + 2)}{2}$ .
- **Пространственная сложность равна $O(1)$, сортировка выполняется на месте**: указатели $i$ и $j$ используют константный объем дополнительной памяти.
- **Нестабильная сортировка**: как показано на рисунке 11-3, элемент `nums[i]` может быть переставлен вправо от другого равного ему элемента, из-за чего их относительный порядок изменится.
+1 -1
View File
@@ -43,7 +43,7 @@ comments: true
**Адаптивность**: <u>адаптивная сортировка</u> умеет использовать уже существующий порядок входных данных, чтобы сократить вычисления и добиться лучшей эффективности. Лучшая временная сложность адаптивных алгоритмов обычно лучше их средней временной сложности.
**Основанность на сравнении**: <u>сортировка на основе сравнений</u> использует операторы сравнения ($<$, $=$, $>$), чтобы определить относительный порядок элементов и отсортировать массив; ее теоретически лучшая временная сложность равна $O(n \log n)$ . А вот <u>сортировка без сравнений</u> не опирается на операторы сравнения, поэтому может достигать $O(n)$ , но универсальность у нее ниже.
**Основанность на сравнении**: <u>сортировка на основе сравнений</u> использует операторы сравнения ($<$, $=$, $>$), чтобы определить относительный порядок элементов и отсортировать массив. Ее теоретически лучшая временная сложность равна $O(n \log n)$ . А вот <u>сортировка без сравнений</u> не опирается на операторы сравнения, поэтому может достигать $O(n)$ , но универсальность у нее ниже.
## 11.1.2 &nbsp; Идеальный алгоритм сортировки
+9 -9
View File
@@ -9,9 +9,9 @@ comments: true
- Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до $O(n)$ .
- Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна $O(n^2)$ , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.
- Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до $O(n^2)$ . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до $O(\log n)$ .
- Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии "разделяй и властвуй". Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна $O(n)$ ; однако при сортировке связного списка пространственную сложность можно оптимизировать до $O(1)$ .
- Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию "разделяй и властвуй" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
- Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
- Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии «разделяй и властвуй». Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна $O(n)$. Однако при сортировке связного списка пространственную сложность можно оптимизировать до $O(1)$ .
- Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию «разделяй и властвуй» и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.
- Сортировка подсчетом является частным случаем блочной сортировки. Она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.
- Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.
- В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, выполнением на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.
- На рисунке 11-19 сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.
@@ -28,21 +28,21 @@ comments: true
Нетрудно увидеть, что в этом случае студенты D и C поменялись местами, порядок по имени разрушился, а именно этого мы и не хотим.
**В**: Можно ли поменять местами порядок "поиска справа налево" и "поиска слева направо" в разделении с опорным элементом?
**В**: Можно ли поменять местами порядок «поиска справа налево» и «поиска слева направо» в разделении с опорным элементом?
Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять "поиск справа налево", а уже затем - "поиск слева направо". Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.
Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять «поиск справа налево», а уже затем - «поиск слева направо». Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.
Последний шаг `partition()` - это обмен `nums[left]` и `nums[i]` . После обмена все элементы слева от опорного должны быть `<=` опорного, **а значит, перед этим обменом должно выполняться условие `nums[left] >= nums[i]`**. Если сначала выполнять "поиск слева направо", то в случае, когда не удается найти элемент больше опорного, **цикл завершится в состоянии `i == j` , и при этом может оказаться, что `nums[j] == nums[i] > nums[left]`**. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.
Последний шаг `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]` , а это неправильный результат.
Например, для массива `[0, 0, 0, 0, 1]` , если сначала выполнять «поиск слева направо», после разделения получится `[1, 0, 0, 0, 0]` , а это неправильный результат.
Если же выбрать `nums[right]` в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять "поиск слева направо".
Если же выбрать `nums[right]` в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять «поиск слева направо».
**В**: Почему при оптимизации глубины рекурсии в быстрой сортировке выбор короткого массива гарантирует, что глубина рекурсии не превысит $\log n$ ?
Глубина рекурсии - это число текущих рекурсивных вызовов, которые еще не завершились. На каждом раунде разделения исходный массив разбивается на два подмассива. После оптимизации глубины рекурсии длина подмассива, в который мы продолжаем рекурсивный спуск, не превышает половины длины исходного массива. Если рассматривать худший случай, когда длина каждый раз становится ровно вдвое меньше, итоговая глубина рекурсии и будет равна $\log n$ .
В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов; в худшем случае это будут длины $n$ , $n - 1$ , $\dots$ , $2$ , $1$ , а глубина рекурсии окажется равной $n$ . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.
В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов. В худшем случае это будут длины $n$ , $n - 1$ , $\dots$ , $2$ , $1$ , а глубина рекурсии окажется равной $n$ . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.
**В**: Если все элементы массива равны, будет ли временная сложность быстрой сортировки равна $O(n^2)$ ? Как справиться с таким вырождением?