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