This commit is contained in:
krahets
2026-04-14 18:06:19 +08:00
parent 17b2a0b630
commit cf0747ba3e
131 changed files with 604 additions and 609 deletions
+9 -9
View File
@@ -4362,9 +4362,9 @@
<li>Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до <span class="arithmatex">\(O(n)\)</span> .</li>
<li>Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна <span class="arithmatex">\(O(n^2)\)</span> , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.</li>
<li>Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до <span class="arithmatex">\(O(n^2)\)</span> . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до <span class="arithmatex">\(O(\log n)\)</span> .</li>
<li>Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии "разделяй и властвуй". Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна <span class="arithmatex">\(O(n)\)</span> ; однако при сортировке связного списка пространственную сложность можно оптимизировать до <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию "разделяй и властвуй" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.</li>
<li>Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.</li>
<li>Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии «разделяй и властвуй». Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна <span class="arithmatex">\(O(n)\)</span>. Однако при сортировке связного списка пространственную сложность можно оптимизировать до <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию «разделяй и властвуй» и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.</li>
<li>Сортировка подсчетом является частным случаем блочной сортировки. Она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.</li>
<li>Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.</li>
<li>В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, выполнением на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.</li>
<li>На рисунке 11-19 сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.</li>
@@ -4376,14 +4376,14 @@
<p><strong>В</strong>: В каких случаях стабильность алгоритма сортировки является обязательной?</p>
<p>В реальных задачах нам может понадобиться сортировать объекты по некоторому атрибуту. Например, у студентов есть два атрибута: имя и рост. Мы хотим выполнить многоуровневую сортировку: сначала отсортировать по имени и получить <code>(A, 180) (B, 185) (C, 170) (D, 170)</code> , а затем отсортировать по росту. Если используемый алгоритм сортировки нестабилен, то мы можем получить <code>(D, 170) (C, 170) (A, 180) (B, 185)</code> .</p>
<p>Нетрудно увидеть, что в этом случае студенты D и C поменялись местами, порядок по имени разрушился, а именно этого мы и не хотим.</p>
<p><strong>В</strong>: Можно ли поменять местами порядок "поиска справа налево" и "поиска слева направо" в разделении с опорным элементом?</p>
<p>Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять "поиск справа налево", а уже затем - "поиск слева направо". Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.</p>
<p>Последний шаг <code>partition()</code> - это обмен <code>nums[left]</code> и <code>nums[i]</code> . После обмена все элементы слева от опорного должны быть <code>&lt;=</code> опорного, <strong>а значит, перед этим обменом должно выполняться условие <code>nums[left] &gt;= nums[i]</code></strong>. Если сначала выполнять "поиск слева направо", то в случае, когда не удается найти элемент больше опорного, <strong>цикл завершится в состоянии <code>i == j</code> , и при этом может оказаться, что <code>nums[j] == nums[i] &gt; nums[left]</code></strong>. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.</p>
<p>Например, для массива <code>[0, 0, 0, 0, 1]</code> , если сначала выполнять "поиск слева направо", после разделения получится <code>[1, 0, 0, 0, 0]</code> , а это неправильный результат.</p>
<p>Если же выбрать <code>nums[right]</code> в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять "поиск слева направо".</p>
<p><strong>В</strong>: Можно ли поменять местами порядок «поиска справа налево» и «поиска слева направо» в разделении с опорным элементом?</p>
<p>Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять «поиск справа налево», а уже затем - «поиск слева направо». Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.</p>
<p>Последний шаг <code>partition()</code> - это обмен <code>nums[left]</code> и <code>nums[i]</code> . После обмена все элементы слева от опорного должны быть <code>&lt;=</code> опорного, <strong>а значит, перед этим обменом должно выполняться условие <code>nums[left] &gt;= nums[i]</code></strong>. Если сначала выполнять «поиск слева направо», то в случае, когда не удается найти элемент больше опорного, <strong>цикл завершится в состоянии <code>i == j</code> , и при этом может оказаться, что <code>nums[j] == nums[i] &gt; nums[left]</code></strong>. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.</p>
<p>Например, для массива <code>[0, 0, 0, 0, 1]</code> , если сначала выполнять «поиск слева направо», после разделения получится <code>[1, 0, 0, 0, 0]</code> , а это неправильный результат.</p>
<p>Если же выбрать <code>nums[right]</code> в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять «поиск слева направо».</p>
<p><strong>В</strong>: Почему при оптимизации глубины рекурсии в быстрой сортировке выбор короткого массива гарантирует, что глубина рекурсии не превысит <span class="arithmatex">\(\log n\)</span> ?</p>
<p>Глубина рекурсии - это число текущих рекурсивных вызовов, которые еще не завершились. На каждом раунде разделения исходный массив разбивается на два подмассива. После оптимизации глубины рекурсии длина подмассива, в который мы продолжаем рекурсивный спуск, не превышает половины длины исходного массива. Если рассматривать худший случай, когда длина каждый раз становится ровно вдвое меньше, итоговая глубина рекурсии и будет равна <span class="arithmatex">\(\log n\)</span> .</p>
<p>В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов; в худшем случае это будут длины <span class="arithmatex">\(n\)</span> , <span class="arithmatex">\(n - 1\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span> , а глубина рекурсии окажется равной <span class="arithmatex">\(n\)</span> . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.</p>
<p>В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов. В худшем случае это будут длины <span class="arithmatex">\(n\)</span> , <span class="arithmatex">\(n - 1\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span> , а глубина рекурсии окажется равной <span class="arithmatex">\(n\)</span> . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.</p>
<p><strong>В</strong>: Если все элементы массива равны, будет ли временная сложность быстрой сортировки равна <span class="arithmatex">\(O(n^2)\)</span> ? Как справиться с таким вырождением?</p>
<p>Да. Для этого случая можно рассмотреть разделение массива на три части: элементы меньше опорного, равные опорному и большие опорного. Рекурсию нужно продолжать только для частей меньше и больше опорного. При таком подходе массив, целиком состоящий из одинаковых элементов, будет отсортирован всего за один раунд разделения.</p>
<p><strong>В</strong>: Почему худшая временная сложность блочной сортировки равна <span class="arithmatex">\(O(n^2)\)</span> ?</p>