mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-12 23:36:06 +00:00
First version.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Сортировка пузырьком
|
||||
|
||||
> *Сортировка пузырьком* реализует сортировку путем последовательного срав- нения и обмена соседних элементов. Этот процесс напоминает подъем пу- зырьков со дна на поверхность, отсюда и такое название.
|
||||
>
|
||||
> Процесс поднятия пузырька можно смоделировать операцией обмена элементов: начиная с самого левого конца массива, производится последо- вательное сравнение соседних элементов, и, если левый элемент \> правый элемент, они меняются местами, как показано на рис. 11.4. После заверше- ния прохода наибольший элемент будет перемещен в самый правый конец массива.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
> **Рис. 11.4.** Моделирование поднятия пузырька с помощью обмена элементов. Шаги 1--4
|
||||
>
|
||||
> 
|
||||
|
||||

|
||||
|
||||
> **Рис. 11.4.** *Окончание*. Шаги 5--7
|
||||
|
||||
## Алгоритм
|
||||
|
||||
> Пусть дан массив длиной *n*, тогда сортировка пузырьком выглядит следующим образом (см. рис. 11.5):
|
||||
|
||||
1) сначала выполняется пузырек для *n* элементов, **перемещая наиболь- ший элемент в правильное положение**;
|
||||
|
||||
2) затем выполняется пузырек для оставшихся *n* -- 1 элементов, **переме- щая второй по величине элемент в правильное положение**;
|
||||
|
||||
3) таким образом, после *n* -- 1 итераций пузырька первые *n* -- 1 **наиболь- ших элементов перемещены в правильные положения**;
|
||||
|
||||
4) единственный оставшийся элемент обязательно является наименьшим, поэтому сортировка массива завершена.
|
||||
|
||||
> 
|
||||
>
|
||||
> **Рис. 11.5.** Процесс сортировки пузырьком
|
||||
>
|
||||
> Ниже приведен пример кода.
|
||||
>
|
||||
> \# === File: bubble_sort.py === def bubble_sort(nums: list\[int\]):
|
||||
>
|
||||
> \"\"\" Сортировка пузырьком.\"\"\" n = len(nums)
|
||||
>
|
||||
> \# Внешний цикл: неотсортированный диапазон \[0, i\]. for i in range(n - 1, 0, -1):
|
||||
>
|
||||
> \# Внутренний цикл: перемещение наибольшего элемента в неотсортированном
|
||||
>
|
||||
> \# диапазоне \[0, i\] в его правый конец. for j in range(i):
|
||||
>
|
||||
> if nums\[j\] \> nums\[j + 1\]:
|
||||
>
|
||||
> \# Обмен nums\[j\] и nums\[j + 1\].
|
||||
>
|
||||
> nums\[j\], nums\[j + 1\] = nums\[j + 1\], nums\[j\]
|
||||
|
||||
## Оптимизация эффективности
|
||||
|
||||
> Если в какой-либо итерации пузырька не выполняется ни одной операции об- мена, это означает, что массив уже отсортирован, и можно сразу вернуть ре- зультат. Поэтому можно добавить флаг flag для отслеживания этой ситуации, и как только она возникнет, немедленно выйти из цикла.
|
||||
>
|
||||
> После оптимизации наихудшая и средняя временные сложности сортиров- ки пузырьком остаются *O*(*n*2); однако, если входной массив полностью отсо- ртирован, можно достичь лучшей временной сложности *O*(*n*).
|
||||
>
|
||||
> \# === File: bubble_sort.py ===
|
||||
>
|
||||
> def bubble_sort_with_flag(nums: list\[int\]):
|
||||
>
|
||||
> \"\"\" Сортировка пузырьком (оптимизация с флагом).\"\"\" n = len(nums)
|
||||
>
|
||||
> \# Внешний цикл: неотсортированный диапазон \[0, i\]. for i in range(n - 1, 0, -1):
|
||||
>
|
||||
> flag = False \# Инициализация флага.
|
||||
>
|
||||
> \# Внутренний цикл: перемещение наибольшего элемента в неотсортированном \# диапазоне \[0, i\] в его правый конец.
|
||||
>
|
||||
> for j in range(i):
|
||||
>
|
||||
> if nums\[j\] \> nums\[j + 1\]:
|
||||
>
|
||||
> \# Обмен nums\[j\] и nums\[j + 1\]
|
||||
>
|
||||
> nums\[j\], nums\[j + 1\] = nums\[j + 1\], nums\[j\] flag = True \# Запись обмена элементов
|
||||
>
|
||||
> if not flag:
|
||||
>
|
||||
> break \# В этой итерации \"пузырька\" не было обмена, выход из цикла.
|
||||
|
||||
## Характеристики алгоритма
|
||||
|
||||
- **Временная сложность** *O*(*n*2), **адаптивная сортировка**: длина масси- ва, проходящего каждую итерацию пузырька, последовательно равна *n* -- 1, *n* -- 2, \..., 2, 1. Сумма этих значений равна (*n* -- 1)*n*/2. После вве- дения оптимизации с флагом лучшая временная сложность может до- стигать *O*(*n*).
|
||||
|
||||
- **Пространственная сложность** *O*(1), **сортировка на месте**: указатели *i* и *j* используют дополнительную память постоянного размера.
|
||||
|
||||
- **Стабильная сортировка**: поскольку при сортировке пузырьком равные элементы не меняются местами.
|
||||
@@ -0,0 +1,47 @@
|
||||
```markdown
|
||||
# Сортировка корзинами
|
||||
|
||||
Несколько предыдущих алгоритмов сортировки относятся к "алгоритмам сортировки на основе сравнения", которые реализуют сортировку путем сравнения размеров элементов. Временная сложность таких алгоритмов сортировки не может превысить $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 + k)$, сортировка не на месте**: требуется дополнительное пространство для $k$ корзин и в общей сложности $n$ элементов.
|
||||
- Стабильность сортировки корзинами зависит от того, является ли стабильным алгоритм сортировки элементов внутри корзин.
|
||||
|
||||
## Как реализовать равномерное распределение
|
||||
|
||||
Временная сложность сортировки корзинами теоретически может достигать $O(n)$, **ключевым моментом является равномерное распределение элементов по корзинам**, поскольку реальные данные часто распределены неравномерно. Например, если мы хотим равномерно распределить все товары на Taobao по ценовым диапазонам в 10 корзин, но распределение цен товаров неравномерно: очень много товаров дешевле 100 юаней, очень мало дороже 1000 юаней. Если разделить ценовой диапазон равномерно на 10 частей, разница в количестве товаров в корзинах будет очень большой.
|
||||
|
||||
Для реализации равномерного распределения мы можем сначала установить приблизительную границу раздела, грубо распределив данные по 3 корзинам. **После завершения распределения корзины с большим количеством товаров продолжают делиться на 3 корзины, пока количество элементов во всех корзинах не станет примерно равным**.
|
||||
|
||||
Как показано на рисунке ниже, этот метод по сути создает рекурсивное дерево, цель которого -- сделать значения листовых узлов максимально равномерными. Конечно, не обязательно каждый раз делить данные на 3 корзины, конкретный способ деления можно гибко выбирать в зависимости от характеристик данных.
|
||||
|
||||

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

|
||||
```
|
||||
@@ -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`. Как следует из названия, префиксная сумма `prefix[i]` по индексу `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` содержит отсортированный результат, и в конце можно заменить исходный массив `nums` массивом `res`. На следующем рисунке показан полный процесс сортировки подсчетом.
|
||||
|
||||
=== "<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)$.
|
||||
@@ -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 @@
|
||||
# Сортировка
|
||||
|
||||

|
||||
|
||||
#### Алгоритмы сортировки
|
||||
|
||||
> *Алгоритмы сортировки* используются для упорядочивания набора данных в определенном порядке. Они имеют широкое применение, поскольку упоря- доченные данные обычно можно более эффективно анализировать, обрабаты- вать и выполнять в них поиск.
|
||||
>
|
||||
> Типы данных в алгоритмах сортировки могут быть целыми числами, числа- ми с плавающей запятой, символами или строками, как показано на рис. 11.1. Правила сортировки могут быть установлены в зависимости от потребностей, например по величине чисел, порядку ASCII-кодов символов или произволь- ным пользовательским правилам.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Сортировка вставками
|
||||
|
||||
> *Сортировка вставками* -- это простой алгоритм сортировки, работа которого схожа с процессом ручной сортировки карт в колоде.
|
||||
>
|
||||
> Более конкретно: в неотсортированном сегменте выбирается опорный эле- мент, который сравнивается по величине с элементами в отсортированном сегменте слева и вставляется на правильное место.
|
||||
>
|
||||
> На рис. 11.6 иллюстрируется процесс вставки элемента в массив. Пусть опорный элемент обозначен как base, необходимо сдвинуть все элементы от целевого индекса до base вправо на одну позицию, затем присвоить base целе- вому индексу.
|
||||
|
||||

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

|
||||
|
||||
> **Рис. 11.7.** Процесс сортировки вставками
|
||||
|
||||
```src
|
||||
[file]{insertion_sort}-[class]{}-[func]{insertion_sort}
|
||||
```
|
||||
|
||||
## Характеристики алгоритма
|
||||
|
||||
- **Временная сложность** *O*(*n*²), **адаптивная сортировка**: в наихудшем случае каждая операция вставки требует соответственно *n* -- 1, *n* -- 2, ..., 2, 1 итераций цикла, сумма которых равна (*n* -- 1)*n*/2, следовательно, временная сложность составляет *O*(*n*²). При работе с отсортированными данными операция вставки завершается досрочно. Когда входной массив полностью отсортирован, сортировка вставками достигает лучшей временной сложности *O*(*n*).
|
||||
|
||||
- **Пространственная сложность** *O*(1), **сортировка на месте**: указатели *i* и *j* используют дополнительное пространство постоянного размера.
|
||||
|
||||
- **Стабильная сортировка**: в процессе операции вставки элемент вставляется справа от равных ему элементов, не изменяя их порядок.
|
||||
|
||||
## Преимущества сортировки вставками
|
||||
|
||||
Временная сложность сортировки вставками составляет *O*(*n*²), в то время как временная сложность быстрой сортировки, которую мы скоро изучим, составляет *O*(*n* log *n*). Несмотря на более высокую временную сложность, **при небольших объемах данных сортировка вставками обычно работает быстрее**.
|
||||
|
||||
Этот вывод аналогичен выводу о применимости линейного и бинарного поиска. Алгоритмы класса *O*(*n* log *n*)*, такие как быстрая сортировка, относятся к алгоритмам сортировки, основанным на стратегии "разделяй и властвуй", и часто включают больше элементарных вычислительных операций. При небольших объемах данных значения *n*² и *n* log *n* достаточно близки, сложность не играет доминирующей роли, и количество элементарных операций в каждом раунде становится решающим фактором.
|
||||
|
||||
Фактически, многие языки программирования (например, Java) используют сортировку вставками во встроенных функциях сортировки, общая идея такова: для длинных массивов применяются алгоритмы сортировки, основанные на стратегии "разделяй и властвуй", например быстрая сортировка; для коротких массивов непосредственно используется сортировка вставками.
|
||||
|
||||
Хотя временная сложность сортировки пузырьком, сортировки выбором и сортировки вставками составляет *O*(*n*²), на практике **частота использования сортировки вставками значительно выше, чем сортировки пузырьком и сортировки выбором**, главным образом по следующим причинам:
|
||||
|
||||
- Сортировка пузырьком реализуется на основе обмена элементов, требует использования временной переменной и включает 3 элементарные операции; сортировка вставками реализуется на основе присваивания элементов и требует только 1 элементарную операцию. Следовательно, **вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками**.
|
||||
|
||||
- Временная сложность сортировки выбором в любом случае составляет *O*(*n*²). **При работе с частично отсортированными данными сортировка вставками обычно более эффективна, чем сортировка выбором**.
|
||||
|
||||
- Сортировка выбором нестабильна и не может применяться для многоуровневой сортировки.
|
||||
@@ -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).
|
||||
|
||||
- **Этап разделения**: можно использовать «итерацию» вместо «рекурсии» для реализации разделения связного списка, тем самым экономя пространство стекового кадра, используемое рекурсией.
|
||||
- **Этап слияния**: в связном списке операции добавления и удаления узлов могут быть реализованы простым изменением ссылок (указателей), поэтому этап слияния (объединение двух коротких упорядоченных связных списков в один длинный упорядоченный связный список) не требует создания дополнительного связного списка.
|
||||
|
||||
Конкретные детали реализации довольно сложны, заинтересованные читатели могут изучить соответствующие материалы для дальнейшего обучения.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Быстрая сортировка
|
||||
|
||||
<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 "Стратегия «разделяй и властвуй» быстрой сортировки"
|
||||
|
||||
Суть разделения с опорным элементом заключается в упрощении задачи сортировки более длинного массива до задачи сортировки двух более коротких массивов.
|
||||
|
||||
```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*²).
|
||||
- **Пространственная сложность** *O*(*n*), **сортировка на месте**: в случае полностью обратного порядка входного массива достигается наихудшая глубина рекурсии *n*, используется *O*(*n*) пространства стека. Операция сортировки выполняется на исходном массиве без использования дополнительных массивов.
|
||||
- **Нестабильная сортировка**: на последнем шаге разделения с опорным элементом опорное число может быть перемещено вправо от равных ему элементов.
|
||||
|
||||
## Почему быстрая сортировка быстрая
|
||||
|
||||
Из названия видно, что быстрая сортировка должна иметь определенные преимущества в эффективности. Хотя средняя временная сложность быстрой сортировки такая же, как у «сортировки слиянием» и «пирамидальной сортировки», обычно быстрая сортировка более эффективна, главным образом по следующим причинам.
|
||||
|
||||
- **Вероятность возникновения худшего случая очень мала**: хотя худшая временная сложность быстрой сортировки составляет *O*(*n*²) и не так стабильна, как сортировка слиянием, в подавляющем большинстве случаев быстрая сортировка может работать с временной сложностью *O*(*n* log *n*).
|
||||
- **Высокая эффективность использования кэша**: при выполнении операции разделения с опорным элементом система может загрузить весь подмассив в кэш, поэтому доступ к элементам более эффективен. В то время как алгоритмы типа «пирамидальной сортировки» требуют скачкообразного доступа к элементам, что лишает их этого свойства.
|
||||
- **Малый константный коэффициент сложности**: среди вышеупомянутых трех алгоритмов быстрая сортировка имеет наименьшее общее количество операций сравнения, присваивания, обмена и т. д. Это похоже на причину, по которой «сортировка вставками» быстрее «сортировки пузырьком».
|
||||
|
||||
## Оптимизация опорного числа
|
||||
|
||||
**Временная эффективность быстрой сортировки может снижаться при некоторых входных данных**. Приведем крайний пример: предположим, что входной массив полностью обратно отсортирован, и поскольку мы выбираем самый левый элемент в качестве опорного числа, то после завершения разделения с опорным элементом опорное число меняется местами с самым правым концом массива, что приводит к длине левого подмассива *n* -- 1 и длине правого подмассива 0. При такой рекурсии после каждого раунда разделения с опорным элементом один подмассив имеет длину 0, стратегия «разделяй и властвуй» становится неэффективной, и быстрая сортировка вырождается в форму, близкую к «сортировке пузырьком».
|
||||
|
||||
Чтобы максимально избежать такой ситуации, **мы можем оптимизировать стратегию выбора опорного числа при разделении с опорным элементом**. Например, мы можем случайно выбрать элемент в качестве опорного числа. Однако, если не повезет и каждый раз будет выбрано неидеальное опорное число, эффективность все равно будет неудовлетворительной.
|
||||
|
||||
Следует отметить, что языки программирования обычно генерируют «псевдослучайные числа». Если мы создадим специальный тестовый пример для последовательности псевдослучайных чисел, эффективность быстрой сортировки все равно может ухудшиться.
|
||||
|
||||
Для дальнейшего улучшения мы можем выбрать три элемента-кандидата в массиве (обычно это первый, последний и средний элементы массива) и **использовать медиану этих трех элементов-кандидатов в качестве опорного числа**. Таким образом, вероятность того, что опорное число будет «ни слишком маленьким, ни слишком большим», значительно возрастет. Конечно, мы также можем выбрать больше элементов-кандидатов для дальнейшего повышения надежности алгоритма. При использовании этого метода вероятность ухудшения временной сложности до *O*(*n*²) значительно снижается.
|
||||
|
||||
Пример кода приведен ниже:
|
||||
|
||||
```src
|
||||
[file]{quick_sort}-[class]{quick_sort_median}-[func]{partition}
|
||||
```
|
||||
|
||||
## Оптимизация глубины рекурсии
|
||||
|
||||
**При некоторых входных данных быстрая сортировка может занимать больше пространства**. Возьмем в качестве примера полностью отсортированный входной массив: пусть длина подмассива в рекурсии равна *m*, каждая операция разделения с опорным элементом будет создавать левый подмассив длиной 0 и правый подмассив длиной *m* -- 1, это означает, что размер задачи, уменьшаемый на каждом уровне рекурсивного вызова, очень мал (уменьшается только на один элемент), высота дерева рекурсии достигнет *n* -- 1, в этом случае потребуется пространство стека размером *O*(*n*).
|
||||
|
||||
Чтобы предотвратить накопление пространства стека, мы
|
||||
@@ -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$ соответственно.
|
||||
- **Стабильная сортировка**: когда сортировка подсчетом стабильна, поразрядная сортировка также стабильна; когда сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать получение правильного результата сортировки.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Сортировка выбором
|
||||
|
||||
> Принцип работы *сортировки выбором* весьма прост: запускается цикл, в каж- дой итерации которого из неотсортированной части массива выбирается наи- меньший элемент и помещается в конец отсортированной части.
|
||||
>
|
||||
> Пусть длина массива равна *n*, алгоритм сортировки выбором заключается в следующем (см. рис. 11.2):
|
||||
|
||||
1) в начальном состоянии все элементы не отсортированы, т. е. неотсорти- рованный (индексный) диапазон равен \[0, *n* -- 1\];
|
||||
|
||||
2) выбирается наименьший элемент из диапазона \[0, *n* -- 1\] и меняется ме- стами с элементом с индексом 0. После этого первый элемент массива отсортирован;
|
||||
|
||||
3) выбирается наименьший элемент из диапазона \[1, *n* -- 1\] и меняется ме- стами с элементом с индексом 1. После этого первые два элемента мас- сива отсортированы;
|
||||
|
||||
4) таким образом, после *n* -- 1 итераций выбора и обмена первые *n* -- 1 эле- ментов массива отсортированы;
|
||||
|
||||
5) единственный оставшийся элемент обязательно является наибольшим, поэтому сортировка массива завершена.
|
||||
|
||||

|
||||
|
||||
> **Рис. 11.2.** Этапы сортировки выбором. Шаги 1--3
|
||||
>
|
||||
> 
|
||||
|
||||

|
||||
|
||||
> **Рис. 11.2.** *Продолжение*. Шаги 4--6
|
||||
>
|
||||
> 
|
||||
|
||||

|
||||
|
||||
> **Рис. 11.2.** *Продолжение*. Шаги 7--9
|
||||
>
|
||||
> 
|
||||
|
||||

|
||||
|
||||
> **Рис. 11.2.** *Окончание*. Шаги 10--11
|
||||
>
|
||||
> В приведенном ниже коде реализации используется переменная *k* для запи- си индекса наименьшего элемента в неотсортированном диапазоне.
|
||||
|
||||
```src
|
||||
[file]{selection_sort}-[class]{}-[func]{selection_sort}
|
||||
```
|
||||
|
||||
## Характеристики алгоритма
|
||||
|
||||
- **Временная сложность** *O*(*n*2), **неадаптивная сортировка**: внешний цикл выполняется *n* -- 1 раз, длина неотсортированного диапазона на первой итерации равна *n*, на последней -- 2, т. е. каждый внешний цикл включает *n*, *n* -- 1, \..., 3, 2 итераций внутреннего цикла, сумма которых равна (*n* -- 1)(*n* + 2)/2.
|
||||
|
||||
- **Пространственная сложность** *O*(1), **сортировка на месте**: указате- ли *i* и *j* используют дополнительное пространство постоянного раз- мера.
|
||||
|
||||
- **Нестабильная сортировка**: как показано на рис. 11.3, элемент nums\[i\] может быть перемещен вправо от равного ему элемента, что изменяет их относительный порядок.
|
||||
|
||||

|
||||
|
||||
> **Рис. 11.3.** Пример нестабильности сортировки выбором
|
||||
@@ -0,0 +1,46 @@
|
||||
# Алгоритмы сортировки
|
||||
|
||||
*Алгоритмы сортировки* используются для упорядочивания набора данных в определенном порядке. Они имеют широкое применение, поскольку упорядоченные данные обычно можно более эффективно анализировать, обрабатывать и выполнять в них поиск.
|
||||
|
||||
Типы данных в алгоритмах сортировки могут быть целыми числами, числами с плавающей запятой, символами или строками, как показано на рис. 11.1. Правила сортировки могут быть установлены в зависимости от потребностей, например по величине чисел, порядку ASCII-кодов символов или произвольным пользовательским правилам.
|
||||
|
||||

|
||||
|
||||
## Критерии оценки
|
||||
|
||||
**Эффективность выполнения**: ожидается, что временная сложность алгоритма сортировки будет как можно ниже, а общее количество операций -- минимальным (уменьшение константного множителя во временной сложности). Для больших объемов данных эффективность выполнения особенно важна.
|
||||
|
||||
**Местность**: как следует из названия, сортировка на месте осуществляется путем непосредственной работы с исходным массивом без использования дополнительных вспомогательных массивов, что позволяет экономить память. Обычно операции перемещения данных при сортировке на месте малочисленны, а скорость выполнения выше.
|
||||
|
||||
**Стабильность**: стабильная сортировка сохраняет относительный порядок равных элементов в массиве после завершения сортировки.
|
||||
|
||||
Стабильная сортировка является необходимым условием для многоуровневой сортировки. Предположим, что у нас есть таблица с информацией о студентах, где 1-й и 2-й столбцы -- это имя и возраст соответственно. В этом случае нестабильная сортировка может привести к потере упорядоченности входных данных.
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
**Адаптивность**: адаптивная сортировка способна использовать имеющуюся информацию о порядке входных данных для уменьшения объема вычислений, достигая более высокой временной эффективности. Лучшая временная сложность адаптивных алгоритмов сортировки обычно превосходит среднюю временную сложность.
|
||||
|
||||
**Основанность на сравнении**: сортировка на основе сравнения использует операторы сравнения (<, =, >) для определения относительного порядка элементов, что позволяет отсортировать весь массив. Теоретическая оптимальная временная сложность составляет *O*(*n* log *n*). В то время как не основанная на сравнении сортировка не использует операторы сравнения, ее временная сложность может достигать *O*(*n*), но ее универсальность относительно ниже.
|
||||
|
||||
## Идеальный алгоритм сортировки
|
||||
|
||||
**Быстрый**, **на месте**, **стабильный**, **адаптивный**, **с хорошей универсальностью**. Очевидно, что до сих пор нет алгоритма сортировки, сочетающего все эти характеристики. Поэтому при выборе алгоритма необходимо учитывать особенности данных и требования задачи.
|
||||
|
||||
Далее мы изучим различные алгоритмы сортировки и проанализируем их достоинства и недостатки на основе вышеуказанных критериев оценки.
|
||||
@@ -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)$.
|
||||
Reference in New Issue
Block a user