HTML Diff
1 added 1 removed
Original 2026-01-01
Modified 2026-02-21
1 <p><a>#статьи</a></p>
1 <p><a>#статьи</a></p>
2 <ul><li>11 мар 2022</li>
2 <ul><li>11 мар 2022</li>
3 <li>0</li>
3 <li>0</li>
4 </ul><h2>Великая тройка: алгоритмы сортировки, которые точно пригодятся на собеседовании</h2>
4 </ul><h2>Великая тройка: алгоритмы сортировки, которые точно пригодятся на собеседовании</h2>
5 <p>Да кто такой этот ваш Bubble Sort?! Валерий Жила доступно рассказал о базовых алгоритмах сортировки и сравнил их характеристики.</p>
5 <p>Да кто такой этот ваш Bubble Sort?! Валерий Жила доступно рассказал о базовых алгоритмах сортировки и сравнил их характеристики.</p>
6 <p>Фото: Getty Images / Brandon Bell</p>
6 <p>Фото: Getty Images / Brandon Bell</p>
7 <p>Онлайн-журнал для тех, кто влюблён в код и информационные технологии. Пишем для айтишников и об айтишниках.</p>
7 <p>Онлайн-журнал для тех, кто влюблён в код и информационные технологии. Пишем для айтишников и об айтишниках.</p>
8 <p>Software engineer, разрабатывает системы управления городской инфраструктурой для мегаполисов по всему миру. Основная деятельность: backend, database engineering. Ведёт твиттер<a>@ValeriiZhyla</a>.</p>
8 <p>Software engineer, разрабатывает системы управления городской инфраструктурой для мегаполисов по всему миру. Основная деятельность: backend, database engineering. Ведёт твиттер<a>@ValeriiZhyla</a>.</p>
9 <p>Сегодня я расскажу простым языком о Bubble Sort, Insertion Sort и Selection Sort. Я покажу, какие идеи лежат в основе этих сортировок и продемонстрирую их сильные и слабые стороны. Мы разберём алгоритмы по шагам, рассмотрим их простые версии и даже немного улучшим.</p>
9 <p>Сегодня я расскажу простым языком о Bubble Sort, Insertion Sort и Selection Sort. Я покажу, какие идеи лежат в основе этих сортировок и продемонстрирую их сильные и слабые стороны. Мы разберём алгоритмы по шагам, рассмотрим их простые версии и даже немного улучшим.</p>
10 <p>Дальнейший рассказ подразумевает, что вас не смущают такие фразы, как "сложность worst-case-алгоритма по времени равна O(n^2)". Иногда Time Complexity я буду называть "сложностью по времени", а Space Complexity - "сложностью по памяти".</p>
10 <p>Дальнейший рассказ подразумевает, что вас не смущают такие фразы, как "сложность worst-case-алгоритма по времени равна O(n^2)". Иногда Time Complexity я буду называть "сложностью по времени", а Space Complexity - "сложностью по памяти".</p>
11 <p>Если вы новичок в теории алгоритмов, сначала прочитайте<a>статью</a>Валерия о Big O Notation. В ней он человеческим языком рассказал, что такое алгоритмическая сложность и как её считать.</p>
11 <p>Если вы новичок в теории алгоритмов, сначала прочитайте<a>статью</a>Валерия о Big O Notation. В ней он человеческим языком рассказал, что такое алгоритмическая сложность и как её считать.</p>
12 <p>Давайте подумаем, на какие вопросы нужно ответить программисту, который собирается отсортировать элементы массива.</p>
12 <p>Давайте подумаем, на какие вопросы нужно ответить программисту, который собирается отсортировать элементы массива.</p>
13 <p><strong>Сортировка оригинала.</strong>Недостаток этого решения в том, что мы потеряем изначальный массив. Представляет ли он ценность - зависит от ситуации. Тем не менее это лишний<em></em><a>побочный эффект (side effect)</a>работы алгоритма.</p>
13 <p><strong>Сортировка оригинала.</strong>Недостаток этого решения в том, что мы потеряем изначальный массив. Представляет ли он ценность - зависит от ситуации. Тем не менее это лишний<em></em><a>побочный эффект (side effect)</a>работы алгоритма.</p>
14 <p>Зато нам не придётся выделять память под копию массива. Иначе Space Complexity выросла бы до <em>O(n)</em>. А так имеем<em>O(1)</em>памяти. Красота! Кроме того, массив не придётся копировать, а значит, мы сэкономим<em>O(n)</em>у Time Complexity.</p>
14 <p>Зато нам не придётся выделять память под копию массива. Иначе Space Complexity выросла бы до <em>O(n)</em>. А так имеем<em>O(1)</em>памяти. Красота! Кроме того, массив не придётся копировать, а значит, мы сэкономим<em>O(n)</em>у Time Complexity.</p>
15 <p><strong>Сортировка копии (out-of-place).</strong>Здесь всё наоборот: никаких сторонних эффектов, зато сложность алгоритма по памяти и времени вырастает до <em>O(n)</em>. Забегая вперёд, скажу, что лишнее<em>O(n)</em>времени на копирование не так уж и страшно по сравнению со временем на саму сортировку, которая сожрёт намного больше. А вот дополнительные<em>O(n)</em>памяти - это серьёзный аргумент против out-of-place-алгоритмов.</p>
15 <p><strong>Сортировка копии (out-of-place).</strong>Здесь всё наоборот: никаких сторонних эффектов, зато сложность алгоритма по памяти и времени вырастает до <em>O(n)</em>. Забегая вперёд, скажу, что лишнее<em>O(n)</em>времени на копирование не так уж и страшно по сравнению со временем на саму сортировку, которая сожрёт намного больше. А вот дополнительные<em>O(n)</em>памяти - это серьёзный аргумент против out-of-place-алгоритмов.</p>
16 <em>Кадр: фильм "Звёздные войны: Эпизод 2 - Атака клонов"</em><p>Напишем функцию, которая эффективно переставляет два элемента местами по их индексам:</p>
16 <em>Кадр: фильм "Звёздные войны: Эпизод 2 - Атака клонов"</em><p>Напишем функцию, которая эффективно переставляет два элемента местами по их индексам:</p>
17 def swap(int[] arr, fst_idx, snd_idx): int temp = arr[frst_idx] arr[frst_idx] = arr[snd_idx] arr[snd_idx] = temp<p>Функция<em>swap()</em>получает массив и два индекса. Сначала она сохраняет копию элемента с индексом frst_idx в переменную<em>temp</em>. Затем записывает в элемент с индексом frst_idx содержимое элемента с индексом<em>snd_idx</em>. В этот момент содержимое обоих элементов одинаковое, а оригинальное значение<em>frst_idx</em>хранится в <em>temp</em>. Поэтому мы записываем его в ячейку<em>snd_idx</em>.</p>
17 def swap(int[] arr, fst_idx, snd_idx): int temp = arr[frst_idx] arr[frst_idx] = arr[snd_idx] arr[snd_idx] = temp<p>Функция<em>swap()</em>получает массив и два индекса. Сначала она сохраняет копию элемента с индексом frst_idx в переменную<em>temp</em>. Затем записывает в элемент с индексом frst_idx содержимое элемента с индексом<em>snd_idx</em>. В этот момент содержимое обоих элементов одинаковое, а оригинальное значение<em>frst_idx</em>хранится в <em>temp</em>. Поэтому мы записываем его в ячейку<em>snd_idx</em>.</p>
18 <p>Приведённые выше идентификаторы довольно часто используются в реальных программах. Временные значения хранят в переменной<em>temp </em>(сокращение от <em>temporary</em>, то есть "временный"). fst - это сокращение от first, snd - от second. А слово "индекс" часто обозначают<em>idx</em>, потому что мы, программисты, не любим тратить буквы впустую :)</p>
18 <p>Приведённые выше идентификаторы довольно часто используются в реальных программах. Временные значения хранят в переменной<em>temp </em>(сокращение от <em>temporary</em>, то есть "временный"). fst - это сокращение от first, snd - от second. А слово "индекс" часто обозначают<em>idx</em>, потому что мы, программисты, не любим тратить буквы впустую :)</p>
19 <p>Было бы неплохо начать со сравнения элементов между собой. "Сравнивать-то можно, а что делать дальше?" - спросите вы. Как что? Менять их местами! Кажется, это действительно поможет навести порядок в нашем массиве… ну, или снизить уровень хаоса :)</p>
19 <p>Было бы неплохо начать со сравнения элементов между собой. "Сравнивать-то можно, а что делать дальше?" - спросите вы. Как что? Менять их местами! Кажется, это действительно поможет навести порядок в нашем массиве… ну, или снизить уровень хаоса :)</p>
20 <p>Статья написана на основе<a>треда</a>Валерия в Twitter.</p>
20 <p>Статья написана на основе<a>треда</a>Валерия в Twitter.</p>
21 <p>Будем сравнивать элементы с соседними индексами. Если значение левого элемента больше правого - поменяем их местами. Произвольный индекс массива назовём<em>i</em>. Тогда у правого соседа будет индекс<em>i + 1</em>. Теперь реализуем эту мысль в виде небольшого блока кода:</p>
21 <p>Будем сравнивать элементы с соседними индексами. Если значение левого элемента больше правого - поменяем их местами. Произвольный индекс массива назовём<em>i</em>. Тогда у правого соседа будет индекс<em>i + 1</em>. Теперь реализуем эту мысль в виде небольшого блока кода:</p>
22 if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Этот код - замечательная отправная точка для сортировки массива от меньшего элемента к большему. Но его нужно довести до ума.</p>
22 if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Этот код - замечательная отправная точка для сортировки массива от меньшего элемента к большему. Но его нужно довести до ума.</p>
23 <p>Сперва применим описанные команды к каждому элементу массива. Разумеется, кроме последнего - иначе наша программа вышла бы за пределы массива и выдала ошибку. Отсюда и <em>n - 1</em>:</p>
23 <p>Сперва применим описанные команды к каждому элементу массива. Разумеется, кроме последнего - иначе наша программа вышла бы за пределы массива и выдала ошибку. Отсюда и <em>n - 1</em>:</p>
24 int n = arr.length for i in 0..n-1: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Этот код не отсортирует наш массив, но сделает его более упорядоченным. Вот небольшой пример его работы:</p>
24 int n = arr.length for i in 0..n-1: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Этот код не отсортирует наш массив, но сделает его более упорядоченным. Вот небольшой пример его работы:</p>
25 <em>Скриншот: Skillbox Media</em><p>Обратите внимание: почти все элементы массива сместились в нужную сторону, а самый большой (9) оказался в крайней правой ячейке! На каждой итерации он менялся местами с соседом справа и "всплыл на поверхность".</p>
25 <em>Скриншот: Skillbox Media</em><p>Обратите внимание: почти все элементы массива сместились в нужную сторону, а самый большой (9) оказался в крайней правой ячейке! На каждой итерации он менялся местами с соседом справа и "всплыл на поверхность".</p>
26 <p>Исполним наш код ещё несколько раз:</p>
26 <p>Исполним наш код ещё несколько раз:</p>
27 <em>Скриншот: Skillbox Media</em><p>Всего за три прохода данные заметно упорядочились. Каждый виток гарантирует всплывание одного из элементов: на первом шаге число 9 встало на своё место, на втором - 8, а затем, по счастливой случайности, - сразу четыре элемента.</p>
27 <em>Скриншот: Skillbox Media</em><p>Всего за три прохода данные заметно упорядочились. Каждый виток гарантирует всплывание одного из элементов: на первом шаге число 9 встало на своё место, на втором - 8, а затем, по счастливой случайности, - сразу четыре элемента.</p>
28 <p>Сколько нужно сделать таких перестановок, чтобы гарантированно отсортировать массив? Если каждый проход выталкивает минимум один элемент, то через n проходов мы точно получим отсортированный массив! Даже через<em>n - 1</em>, потому что у крайнего правого элемента останется только один вариант размещения.</p>
28 <p>Сколько нужно сделать таких перестановок, чтобы гарантированно отсортировать массив? Если каждый проход выталкивает минимум один элемент, то через n проходов мы точно получим отсортированный массив! Даже через<em>n - 1</em>, потому что у крайнего правого элемента останется только один вариант размещения.</p>
29 int n = arr.length for j in 0..n-1: for i in 0..n-1: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Великолепно! Мы только что получили первый алгоритм сортировки. Итерация за итерацией - б<strong>о</strong>льшие числа "всплывают", а меньшие "тонут". Очень давно кто-то увидел в этом аналогию с пузырьками воздуха в воде, поэтому такая сортировка называется пузырьковой, или Bubble Sort.</p>
29 int n = arr.length for j in 0..n-1: for i in 0..n-1: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Великолепно! Мы только что получили первый алгоритм сортировки. Итерация за итерацией - б<strong>о</strong>льшие числа "всплывают", а меньшие "тонут". Очень давно кто-то увидел в этом аналогию с пузырьками воздуха в воде, поэтому такая сортировка называется пузырьковой, или Bubble Sort.</p>
30 <p>Наша версия "пузырька" вышла максимально наивной: и в лучшем, и в худшем случае его сложность по времени равна<em>O(n^2)</em>. Конечно, план надёжный как швейцарские часы, но его можно улучшить!</p>
30 <p>Наша версия "пузырька" вышла максимально наивной: и в лучшем, и в худшем случае его сложность по времени равна<em>O(n^2)</em>. Конечно, план надёжный как швейцарские часы, но его можно улучшить!</p>
31 <p>Мы знаем, что каждое выполнение внутреннего цикла гарантированно ставит на место один правый элемент. Значит, с каждым новым j можно сокращать диапазон прохода внутреннего цикла на единицу. Зачем трогать элементы, которые уже стоят на своих местах?</p>
31 <p>Мы знаем, что каждое выполнение внутреннего цикла гарантированно ставит на место один правый элемент. Значит, с каждым новым j можно сокращать диапазон прохода внутреннего цикла на единицу. Зачем трогать элементы, которые уже стоят на своих местах?</p>
32 <p>Поэтому в строке 3 добавляем смещение j:</p>
32 <p>Поэтому в строке 3 добавляем смещение j:</p>
33 int n = arr.length for j in 0..n-1: for i in 0..n-1-j: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Уже лучше, но программа всё ещё делает много лишней работы. Ведь за одну итерацию может "всплыть" сразу несколько элементов. Как добиться, чтобы программа останавливалась, когда массив будет полностью отсортирован? А вот как:</p>
33 int n = arr.length for j in 0..n-1: for i in 0..n-1-j: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1)<p>Уже лучше, но программа всё ещё делает много лишней работы. Ведь за одну итерацию может "всплыть" сразу несколько элементов. Как добиться, чтобы программа останавливалась, когда массив будет полностью отсортирован? А вот как:</p>
34 int n = arr.length int was_swapped = false for j in 0..n-1: was_swapped = false for i in 0..n-1-j: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1) was_swapped = true if was_swapped == false break;<p>Мы ввели новую переменную, которая в начале внешнего цикла устанавливается в false и после каждого свопа меняет значение на true.</p>
34 int n = arr.length int was_swapped = false for j in 0..n-1: was_swapped = false for i in 0..n-1-j: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1) was_swapped = true if was_swapped == false break;<p>Мы ввели новую переменную, которая в начале внешнего цикла устанавливается в false и после каждого свопа меняет значение на true.</p>
35 <p>Если внутренний цикл не сделает ни одного свопа, переменная сохранит значение false и прервёт внешний цикл. Теперь сложность Best Case стала равна O(n)!</p>
35 <p>Если внутренний цикл не сделает ни одного свопа, переменная сохранит значение false и прервёт внешний цикл. Теперь сложность Best Case стала равна O(n)!</p>
36 <p>Если программа работает с изначально отсортированным массивом, то потребуется лишь один проход внутреннего цикла - чтобы обнаружить отсутствие перестановок и прервать внешний цикл. Вот это уже смахивает на Bubble Sort, который и на собесе не стыдно нарисовать!</p>
36 <p>Если программа работает с изначально отсортированным массивом, то потребуется лишь один проход внутреннего цикла - чтобы обнаружить отсутствие перестановок и прервать внешний цикл. Вот это уже смахивает на Bubble Sort, который и на собесе не стыдно нарисовать!</p>
37 <p>Облачим код в функцию и продолжим полёт нашей фантазии:</p>
37 <p>Облачим код в функцию и продолжим полёт нашей фантазии:</p>
38 def bubble_sort(int[] arr) int n = arr.length int was_swapped = false for j in 0..n-1: was_swapped = false for i in 0..n-1-j: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1) was_swapped = true if was_swapped == false break return arr<p>В таблице представлены значения Time Complexity для лучшего, среднего и худшего случаев входных данных, а также значение Space Complexity:</p>
38 def bubble_sort(int[] arr) int n = arr.length int was_swapped = false for j in 0..n-1: was_swapped = false for i in 0..n-1-j: if arr[i] &gt; arr[i + 1]: swap(arr, i, i + 1) was_swapped = true if was_swapped == false break return arr<p>В таблице представлены значения Time Complexity для лучшего, среднего и худшего случаев входных данных, а также значение Space Complexity:</p>
39 - NameBubble<strong>Name</strong>Bubble<strong>Worst</strong>O(n^2)<strong>Average</strong>O(n^2)<strong>Best</strong>O(n)<strong>Space</strong>O(1)<p>В основе пузырьковой сортировки - сравнение соседних элементов массива. А как ещё можно отсортировать массив? Начнём с небольшой воображаемй сцены.</p>
39 + NameBubble<strong>Name</strong>Bubble<strong>Worst</strong>O(n^2)<strong>Average</strong>O(n^2)<strong>Best</strong>O(n)<strong>Space</strong>O(1)<p>В основе пузырьковой сортировки - сравнение соседних элементов массива. А как ещё можно отсортировать массив? Начнём с небольшой воображаемой сцены.</p>
40 <p>Перед мальчиком (можете заменить на девочку) на столе хаотично лежат карточки с числами. Мальчик хочет разложить эти числа по возрастанию. Он находит карточку с самым маленьким числом и откладывает её на соседний стол. Затем он возвращается к перемешанным карточкам, находит очередное наименьшее число, забирает карточку и кладёт её на второй стол, справа от предыдущей.</p>
40 <p>Перед мальчиком (можете заменить на девочку) на столе хаотично лежат карточки с числами. Мальчик хочет разложить эти числа по возрастанию. Он находит карточку с самым маленьким числом и откладывает её на соседний стол. Затем он возвращается к перемешанным карточкам, находит очередное наименьшее число, забирает карточку и кладёт её на второй стол, справа от предыдущей.</p>
41 <p>Так, шаг за шагом, мальчик выстроит шеренгу из чисел. Несложно догадаться, что получившийся аналог массива будет отсортирован по возрастанию - слева лежит наименьшее число, справа - наибольшее. Примерно так работает наивная сортировка выбором, или Selection Sort. Давайте попробуем изобразить её в виде кода.</p>
41 <p>Так, шаг за шагом, мальчик выстроит шеренгу из чисел. Несложно догадаться, что получившийся аналог массива будет отсортирован по возрастанию - слева лежит наименьшее число, справа - наибольшее. Примерно так работает наивная сортировка выбором, или Selection Sort. Давайте попробуем изобразить её в виде кода.</p>
42 <p>Нам понадобится функция для поиска индекса наименьшего элемента:</p>
42 <p>Нам понадобится функция для поиска индекса наименьшего элемента:</p>
43 def find_min(int[] arr): int current_min_idx = 0 int current_min_value = arr[0] for i in 0..arr.length: if (arr[i] &lt; current_min_value): current_min_value = arr[i] current_min_idx = i return current_min_idx<p>Можно было бы обойтись без<em>current_min_value</em>, но я оставлю эту переменную для наглядности.</p>
43 def find_min(int[] arr): int current_min_idx = 0 int current_min_value = arr[0] for i in 0..arr.length: if (arr[i] &lt; current_min_value): current_min_value = arr[i] current_min_idx = i return current_min_idx<p>Можно было бы обойтись без<em>current_min_value</em>, но я оставлю эту переменную для наглядности.</p>
44 <p>Суть<em>find_min()</em>предельно проста. Будем считать, что первый элемент массива - это текущий минимум, и поочерёдно сравним его с другими элементами. Если какой-нибудь элемент меньше текущего минимума, то обновим значение<em>current_min_value</em>.</p>
44 <p>Суть<em>find_min()</em>предельно проста. Будем считать, что первый элемент массива - это текущий минимум, и поочерёдно сравним его с другими элементами. Если какой-нибудь элемент меньше текущего минимума, то обновим значение<em>current_min_value</em>.</p>
45 <p>При этом мы обусловимся, что в массиве arr есть хотя бы один элемент и все ячейки заполнены числами. В противном случае<em>find_min()</em>станет ещё более страшным :)</p>
45 <p>При этом мы обусловимся, что в массиве arr есть хотя бы один элемент и все ячейки заполнены числами. В противном случае<em>find_min()</em>станет ещё более страшным :)</p>
46 <p>Ещё нам нужна функция, которая удаляет элемент из массива. Максимально простая функция, исключительно красоты ради:</p>
46 <p>Ещё нам нужна функция, которая удаляет элемент из массива. Максимально простая функция, исключительно красоты ради:</p>
47 def delete_element(int[] arr, int idx): arr[idx] = null<p>Итак, приготовления завершены, и мы можем приступить к сортировке. Будем урезать оригинальный массив шаг за шагом, а результат выстраивать во втором массиве. Для этого сначала создадим пустой массив того же размера, что и оригинал.</p>
47 def delete_element(int[] arr, int idx): arr[idx] = null<p>Итак, приготовления завершены, и мы можем приступить к сортировке. Будем урезать оригинальный массив шаг за шагом, а результат выстраивать во втором массиве. Для этого сначала создадим пустой массив того же размера, что и оригинал.</p>
48 <p>А дальше реализуем шаги из нашего мысленного эксперимента в виде кода:</p>
48 <p>А дальше реализуем шаги из нашего мысленного эксперимента в виде кода:</p>
49 def native_selection_sort(int[] arr): int n = arr.length int[] sorted_arr = int[n] # пустой массив с n ячейками for i in 0..n: int idx_of_min_element = find_min(arr) sorted_arr[i] = arr[idx_of_min_element] delete_element(arr) return sorted_arr<p>Какие замечания могут возникнуть к этому алгоритму:</p>
49 def native_selection_sort(int[] arr): int n = arr.length int[] sorted_arr = int[n] # пустой массив с n ячейками for i in 0..n: int idx_of_min_element = find_min(arr) sorted_arr[i] = arr[idx_of_min_element] delete_element(arr) return sorted_arr<p>Какие замечания могут возникнуть к этому алгоритму:</p>
50 <ul><li><strong>Он ест слишком много памяти.</strong>Мы создаём отдельный массив под результат и получаем<em>O(n)</em>по памяти.</li>
50 <ul><li><strong>Он ест слишком много памяти.</strong>Мы создаём отдельный массив под результат и получаем<em>O(n)</em>по памяти.</li>
51 <li><strong>В оригинальном массиве появляются дыры.</strong>Функция<em>delete_element()</em>создаёт пустые ячейки в <em>arr</em>, о которые споткнётся<em>find_min()</em>.</li>
51 <li><strong>В оригинальном массиве появляются дыры.</strong>Функция<em>delete_element()</em>создаёт пустые ячейки в <em>arr</em>, о которые споткнётся<em>find_min()</em>.</li>
52 <li><strong>Высокая Time Complexity.</strong>Алгоритм явно не блещет скоростью исполнения -<em>O(n^2)</em>как в худшем, так и в лучшем случае.</li>
52 <li><strong>Высокая Time Complexity.</strong>Алгоритм явно не блещет скоростью исполнения -<em>O(n^2)</em>как в худшем, так и в лучшем случае.</li>
53 </ul><p>Возникает резонный вопрос: что же делать? Сейчас может быть немного непонятно, но обещаю, что дальше всё встанет на свои места.</p>
53 </ul><p>Возникает резонный вопрос: что же делать? Сейчас может быть немного непонятно, но обещаю, что дальше всё встанет на свои места.</p>
54 <p>Вот код оптимальной Selection Sort. Он упорядочивает элементы сразу в <em>arr,</em>а не создаёт дополнительный массив:</p>
54 <p>Вот код оптимальной Selection Sort. Он упорядочивает элементы сразу в <em>arr,</em>а не создаёт дополнительный массив:</p>
55 def selection_sort(int[] arr): int n = arr.length for i in 0..n: min_idx = find_min_between(arr, i + 1, n) swap(arr, i, min_idx) return arr<p>Компактно, правда? Я добавил функцию<em>find_min_between(array, start_idx, end_idx)</em>. Она работает как уже знакомая<em>find_min(array),</em>только проходит не по всему массиву, а по отрезку между<em>start_idx</em>и <em>end_idx</em>. Функция<em>swap(array, i, j)</em>тоже в представлении не нуждается.</p>
55 def selection_sort(int[] arr): int n = arr.length for i in 0..n: min_idx = find_min_between(arr, i + 1, n) swap(arr, i, min_idx) return arr<p>Компактно, правда? Я добавил функцию<em>find_min_between(array, start_idx, end_idx)</em>. Она работает как уже знакомая<em>find_min(array),</em>только проходит не по всему массиву, а по отрезку между<em>start_idx</em>и <em>end_idx</em>. Функция<em>swap(array, i, j)</em>тоже в представлении не нуждается.</p>
56 <p>Теперь разберёмся, как работает улучшенная версия Selection Sort. Возьмём массив<em>arr</em>с десятью числами:</p>
56 <p>Теперь разберёмся, как работает улучшенная версия Selection Sort. Возьмём массив<em>arr</em>с десятью числами:</p>
57 <em>Скриншот: Skillbox Media</em><p>Пройдёмся по массиву несколько раз циклом из <em>selection_sort()</em>и посмотрим на состояние<em>arr</em>. Вот что мы получим:</p>
57 <em>Скриншот: Skillbox Media</em><p>Пройдёмся по массиву несколько раз циклом из <em>selection_sort()</em>и посмотрим на состояние<em>arr</em>. Вот что мы получим:</p>
58 <em>Скриншот: Skillbox Media</em><p>Чтобы сохранять упорядоченные элементы в левой части массива<em>arr</em>, алгоритм использует барьер между отсортированной и неотсортированной частью. Этот барьер находится между элементами с индексами<em>i</em>и <em>(i + 1)</em>.</p>
58 <em>Скриншот: Skillbox Media</em><p>Чтобы сохранять упорядоченные элементы в левой части массива<em>arr</em>, алгоритм использует барьер между отсортированной и неотсортированной частью. Этот барьер находится между элементами с индексами<em>i</em>и <em>(i + 1)</em>.</p>
59 <p>В каждой итерации алгоритм ищет наименьшее значение в правой части (от i + 1 и до конца массива) и меняет его местами с элементом по индексу i. При<em>i = 1</em>в отсортированной части массива будет только один элемент. При i = 2 - два.</p>
59 <p>В каждой итерации алгоритм ищет наименьшее значение в правой части (от i + 1 и до конца массива) и меняет его местами с элементом по индексу i. При<em>i = 1</em>в отсортированной части массива будет только один элемент. При i = 2 - два.</p>
60 <p>Сделаем процесс чуть более наглядным. Добавим разделитель между элементами, по которым проходит граница между отсортированной и неотсортированной частями массива:</p>
60 <p>Сделаем процесс чуть более наглядным. Добавим разделитель между элементами, по которым проходит граница между отсортированной и неотсортированной частями массива:</p>
61 <em>Скриншот: Skillbox Media</em><p>Обратите внимание: состояние<em>arr</em>показывается в конце каждой итерации. Первая строчка при i = 0, вторая при i = 1 и так далее.</p>
61 <em>Скриншот: Skillbox Media</em><p>Обратите внимание: состояние<em>arr</em>показывается в конце каждой итерации. Первая строчка при i = 0, вторая при i = 1 и так далее.</p>
62 <p>Надеюсь, теперь идея Selection Sort кажется простой и естественной. Давайте взглянем на характеристики алгоритма.</p>
62 <p>Надеюсь, теперь идея Selection Sort кажется простой и естественной. Давайте взглянем на характеристики алгоритма.</p>
63 <p>С одной стороны, у такой реализации хорошая сложность по памяти<em>O(1)</em>за счёт использования левой части массива. С другой - поиск минимального элемента съедает<em>O(n)</em>времени и повторяется в каждом цикле (<em>n</em>раз). Поэтому мы получаем и Worst Case, и Best Case равные<em>O(n^2)</em>, а это нехорошо.</p>
63 <p>С одной стороны, у такой реализации хорошая сложность по памяти<em>O(1)</em>за счёт использования левой части массива. С другой - поиск минимального элемента съедает<em>O(n)</em>времени и повторяется в каждом цикле (<em>n</em>раз). Поэтому мы получаем и Worst Case, и Best Case равные<em>O(n^2)</em>, а это нехорошо.</p>
64 <p>Выходит, Bubble Sort быстрее, чем Selection Sort из-за Best Case<em>O(n)</em>? Тут как в известном анекдоте: есть один нюанс. В конце статьи мы ещё обсудим аспекты, которые не учитывает О-нотация.</p>
64 <p>Выходит, Bubble Sort быстрее, чем Selection Sort из-за Best Case<em>O(n)</em>? Тут как в известном анекдоте: есть один нюанс. В конце статьи мы ещё обсудим аспекты, которые не учитывает О-нотация.</p>
65 <p>Дополним нашу таблицу с характеристиками:</p>
65 <p>Дополним нашу таблицу с характеристиками:</p>
66 NameBubbleSelection<strong>Name</strong>BubbleSelection<strong>Worst</strong>O(n^2)O(n^2)<strong>Average</strong>O(n^2)O(n^2)<strong>Best</strong>O(n)O(n^2)<strong>Space</strong>O(1)O(1)<p>Insertion Sort, она же сортировка вставками, очень похожа на Selection Sort. Я даже называю её "Selection Sort наоборот", потому что она действует по обратному принципу.</p>
66 NameBubbleSelection<strong>Name</strong>BubbleSelection<strong>Worst</strong>O(n^2)O(n^2)<strong>Average</strong>O(n^2)O(n^2)<strong>Best</strong>O(n)O(n^2)<strong>Space</strong>O(1)O(1)<p>Insertion Sort, она же сортировка вставками, очень похожа на Selection Sort. Я даже называю её "Selection Sort наоборот", потому что она действует по обратному принципу.</p>
67 <p>Напомню, что в Selection Sort мы искали наименьший элемент в правой части, переставляли его в конец левой и шли дальше. Самая дорогая операция в этом алгоритме - поиск наименьшего элемента.</p>
67 <p>Напомню, что в Selection Sort мы искали наименьший элемент в правой части, переставляли его в конец левой и шли дальше. Самая дорогая операция в этом алгоритме - поиск наименьшего элемента.</p>
68 <p>В Insertion Sort мы тоже сохраняем упорядоченные элементы в исходном массиве. Только на этот раз из правой части берём первый попавшийся элемент и выбираем, куда его вставить в левой.</p>
68 <p>В Insertion Sort мы тоже сохраняем упорядоченные элементы в исходном массиве. Только на этот раз из правой части берём первый попавшийся элемент и выбираем, куда его вставить в левой.</p>
69 <p>При этом левая часть массива всегда будет отсортирована. А это значит, что нам нужно лишь найти подходящее место, сдвинуть всех соседей справа на одну позицию правее и вставить новый элемент на освободившееся место.</p>
69 <p>При этом левая часть массива всегда будет отсортирована. А это значит, что нам нужно лишь найти подходящее место, сдвинуть всех соседей справа на одну позицию правее и вставить новый элемент на освободившееся место.</p>
70 <p>Работа Insertion Sort немного напоминает толкучку в общественном транспорте. Боксёр-тяжеловес, который зайдёт в набитый трамвай, вероятно, спровоцирует движение всех пассажиров на полшага от выхода.</p>
70 <p>Работа Insertion Sort немного напоминает толкучку в общественном транспорте. Боксёр-тяжеловес, который зайдёт в набитый трамвай, вероятно, спровоцирует движение всех пассажиров на полшага от выхода.</p>
71 <p>Рассмотрим пример такой вставки. У нас есть отсортированный массив из восьми чисел со свободной ячейкой в конце. Нужно добавить в этот массив число 5 на правильную позицию. Вот как будет выглядеть алгоритм:</p>
71 <p>Рассмотрим пример такой вставки. У нас есть отсортированный массив из восьми чисел со свободной ячейкой в конце. Нужно добавить в этот массив число 5 на правильную позицию. Вот как будет выглядеть алгоритм:</p>
72 int[] arr = [1, 2, 3, 4, 6, 7, 8, 9, _] int key = 5 int element_to_move_idx = arr.length - 2 # arr[arr.length - 2] = 9 while (element_to_move_idx &gt;= 0) and (key &lt; arr[element_to_move_idx]): arr[element_to_move_idx + 1] = arr[element_to_move_idx] element_to_move_idx-- #вставляем новое значение в пустую ячейку arr[element_to_move_idx + 1] = key<p>Начнём передвигать числа больше 5 вправо:</p>
72 int[] arr = [1, 2, 3, 4, 6, 7, 8, 9, _] int key = 5 int element_to_move_idx = arr.length - 2 # arr[arr.length - 2] = 9 while (element_to_move_idx &gt;= 0) and (key &lt; arr[element_to_move_idx]): arr[element_to_move_idx + 1] = arr[element_to_move_idx] element_to_move_idx-- #вставляем новое значение в пустую ячейку arr[element_to_move_idx + 1] = key<p>Начнём передвигать числа больше 5 вправо:</p>
73 <em>Скриншот: Skillbox Media</em><p>Обратите внимание: справа от передвигаемого числа возникает его копия, а оригинал перезаписывается соседом слева на следующей итерации.</p>
73 <em>Скриншот: Skillbox Media</em><p>Обратите внимание: справа от передвигаемого числа возникает его копия, а оригинал перезаписывается соседом слева на следующей итерации.</p>
74 <p>А вот как выглядит массив после всех сдвиганий:</p>
74 <p>А вот как выглядит массив после всех сдвиганий:</p>
75 <em>Скриншот: Skillbox Media</em><p>Когда цикл дойдёт до 4, он остановится и запишет число 5 вместо "мусорной" шестёрки.</p>
75 <em>Скриншот: Skillbox Media</em><p>Когда цикл дойдёт до 4, он остановится и запишет число 5 вместо "мусорной" шестёрки.</p>
76 <em>Скриншот: Skillbox Media</em><p>Теперь взглянем на заветный код Insertion Sort:</p>
76 <em>Скриншот: Skillbox Media</em><p>Теперь взглянем на заветный код Insertion Sort:</p>
77 def insertion_sort(int[] arr): int n = arr.length for i in 1..n: key = arr[i] # передвигаем в отсортированную часть элементы, которые больше, чем key int element_to_move_idx = i - 1 while (element_to_move_idx &gt;= 0) and (key &lt; arr[element_to_move_idx]): arr[element_to_move_idx + 1] = arr[element_to_move_idx] element_to_move_idx-- #вставляем новое значение в пустую ячейку arr[element_to_move_idx + 1] = key return arr<p>Помните барьер из Selection Sort? Так как алгоритм похож, предлагаю повторить такой же трюк с Insertion Sort:</p>
77 def insertion_sort(int[] arr): int n = arr.length for i in 1..n: key = arr[i] # передвигаем в отсортированную часть элементы, которые больше, чем key int element_to_move_idx = i - 1 while (element_to_move_idx &gt;= 0) and (key &lt; arr[element_to_move_idx]): arr[element_to_move_idx + 1] = arr[element_to_move_idx] element_to_move_idx-- #вставляем новое значение в пустую ячейку arr[element_to_move_idx + 1] = key return arr<p>Помните барьер из Selection Sort? Так как алгоритм похож, предлагаю повторить такой же трюк с Insertion Sort:</p>
78 <em>Скриншот: Skillbox Media</em><p>Нижняя граница производительности довольно привлекательна. Если массив изначально отсортирован, то ничего сдвигать не нужно! Сложность Best Case у такой сортировки составляет<em>O(n)</em>.</p>
78 <em>Скриншот: Skillbox Media</em><p>Нижняя граница производительности довольно привлекательна. Если массив изначально отсортирован, то ничего сдвигать не нужно! Сложность Best Case у такой сортировки составляет<em>O(n)</em>.</p>
79 <p>Мы разобрали "Великую Тройку Сортировок", с которой неизбежно сталкивается любой, кто изучает алгоритмы. С чем я всех и поздравляю!</p>
79 <p>Мы разобрали "Великую Тройку Сортировок", с которой неизбежно сталкивается любой, кто изучает алгоритмы. С чем я всех и поздравляю!</p>
80 <p>Стабильным (устойчивым, stable) называется алгоритм сортировки, который не меняет порядок элементов с одинаковыми ключами относительно друг друга. Если мы сортируем числа, то стабильность особой роли не играет, а вот если сортируем объекты, то очень даже.</p>
80 <p>Стабильным (устойчивым, stable) называется алгоритм сортировки, который не меняет порядок элементов с одинаковыми ключами относительно друг друга. Если мы сортируем числа, то стабильность особой роли не играет, а вот если сортируем объекты, то очень даже.</p>
81 <p>Допустим, нужно отсортировать по возрасту массив с записями о клиентах. Для наглядности возьмём максимально простой вид записей в формате [возраст : имя].</p>
81 <p>Допустим, нужно отсортировать по возрасту массив с записями о клиентах. Для наглядности возьмём максимально простой вид записей в формате [возраст : имя].</p>
82 <em>Скриншот: Skillbox Media</em><p>После стабильной сортировки Кузя гарантированно будет расположен выше Олега, потому что именно так расположены элементы с одинаковыми ключами в оригинальных данных.</p>
82 <em>Скриншот: Skillbox Media</em><p>После стабильной сортировки Кузя гарантированно будет расположен выше Олега, потому что именно так расположены элементы с одинаковыми ключами в оригинальных данных.</p>
83 <em>Скриншот: Skillbox Media</em><p>В нестабильном варианте допускается второй сценарий:</p>
83 <em>Скриншот: Skillbox Media</em><p>В нестабильном варианте допускается второй сценарий:</p>
84 <em>Скриншот: Skillbox Media</em><p>Стабильность крайне важна, если мы планируем сортировать массив объектов по нескольким ключам. Например, сначала по возрасту, потом по размеру банковского счёта и затем по площади жилья. В таком случае нестабильный алгоритм испортит нам всю малину.</p>
84 <em>Скриншот: Skillbox Media</em><p>Стабильность крайне важна, если мы планируем сортировать массив объектов по нескольким ключам. Например, сначала по возрасту, потом по размеру банковского счёта и затем по площади жилья. В таком случае нестабильный алгоритм испортит нам всю малину.</p>
85 <p>В нашей тройке стабильными являются Bubble Sort и Insertion Sort, а вот Selection Sort так и норовит перемешать элементы.</p>
85 <p>В нашей тройке стабильными являются Bubble Sort и Insertion Sort, а вот Selection Sort так и норовит перемешать элементы.</p>
86 <p>Дополним нашу таблицу характеристиками Selection Sort и строкой с показателями стабильности:</p>
86 <p>Дополним нашу таблицу характеристиками Selection Sort и строкой с показателями стабильности:</p>
87 NameBubbleSelectionInsertion<strong>Name</strong>BubbleSelectionInsertion<strong>Worst</strong>O(n^2)O(n^2)O(n^2)<strong>Average</strong>O(n^2)O(n^2)O(n^2)<strong>Best</strong>O(n)O(n^2)O(n)<strong>Space</strong>O(1)O(1)O(1)<strong>Stable</strong>yesnoyes<p>Я обещал обсудить нюансы, которые не учитывает O-нотация. Пришло время перемывать косточки уже полюбившимся алгоритмам!</p>
87 NameBubbleSelectionInsertion<strong>Name</strong>BubbleSelectionInsertion<strong>Worst</strong>O(n^2)O(n^2)O(n^2)<strong>Average</strong>O(n^2)O(n^2)O(n^2)<strong>Best</strong>O(n)O(n^2)O(n)<strong>Space</strong>O(1)O(1)O(1)<strong>Stable</strong>yesnoyes<p>Я обещал обсудить нюансы, которые не учитывает O-нотация. Пришло время перемывать косточки уже полюбившимся алгоритмам!</p>
88 <p>Посмотрим на почти идеально отсортированный массив. Почти - потому что самый большой элемент в нём стоит на первом месте:</p>
88 <p>Посмотрим на почти идеально отсортированный массив. Почти - потому что самый большой элемент в нём стоит на первом месте:</p>
89 <em>Скриншот: Skillbox Media</em><p>Как поведут себя алгоритмы:</p>
89 <em>Скриншот: Skillbox Media</em><p>Как поведут себя алгоритмы:</p>
90 <ul><li><strong>Bubble Sort</strong>сработает великолепно. За <em>n</em>перестановок наибольший элемент "всплывёт" наверх.</li>
90 <ul><li><strong>Bubble Sort</strong>сработает великолепно. За <em>n</em>перестановок наибольший элемент "всплывёт" наверх.</li>
91 <li><strong>Insertion Sort</strong>тоже хорош в этом вопросе.</li>
91 <li><strong>Insertion Sort</strong>тоже хорош в этом вопросе.</li>
92 <li><strong>Selection Sort</strong>со страшной расточительностью перелопатит весь массив и выдаст<em>O(n^2)</em>по времени.</li>
92 <li><strong>Selection Sort</strong>со страшной расточительностью перелопатит весь массив и выдаст<em>O(n^2)</em>по времени.</li>
93 </ul><p><strong>Вывод:</strong>если данные почти отсортированы, то Bubble Sort и Insertion Sort - наши лучшие друзья.</p>
93 </ul><p><strong>Вывод:</strong>если данные почти отсортированы, то Bubble Sort и Insertion Sort - наши лучшие друзья.</p>
94 <p>Теперь подумаем о стоимости операций. В О-нотации об этом ничего не говорится, но на самом деле разные операции требуют разного количества ресурсов компьютера.</p>
94 <p>Теперь подумаем о стоимости операций. В О-нотации об этом ничего не говорится, но на самом деле разные операции требуют разного количества ресурсов компьютера.</p>
95 <p>Например, прочитать значение в ячейке массива - элементарно. Перезаписать ячейку - вроде бы тоже просто, да вот только многопоточность, обновление значений в кэше процессора и другие факторы реального мира с этим не согласны. Поэтому операция<em>swap()</em>не так уж и проста, как кажется на первый взгляд.</p>
95 <p>Например, прочитать значение в ячейке массива - элементарно. Перезаписать ячейку - вроде бы тоже просто, да вот только многопоточность, обновление значений в кэше процессора и другие факторы реального мира с этим не согласны. Поэтому операция<em>swap()</em>не так уж и проста, как кажется на первый взгляд.</p>
96 <p>А вот что у нас по количеству свопов:</p>
96 <p>А вот что у нас по количеству свопов:</p>
97 <ul><li><strong>Bubble Sort</strong>просто монстр, который свопает без остановки. Количество операций записи здесь зашкаливает.</li>
97 <ul><li><strong>Bubble Sort</strong>просто монстр, который свопает без остановки. Количество операций записи здесь зашкаливает.</li>
98 <li><strong>Insertion Sort</strong>тоже не подарок - постоянное сдвигание элементов вправо до добра не доведёт.</li>
98 <li><strong>Insertion Sort</strong>тоже не подарок - постоянное сдвигание элементов вправо до добра не доведёт.</li>
99 <li><strong>Selection Sort</strong> - конфетка! Всего<em>n</em>свопов в худшем случае. Очень экономичный алгоритм.</li>
99 <li><strong>Selection Sort</strong> - конфетка! Всего<em>n</em>свопов в худшем случае. Очень экономичный алгоритм.</li>
100 </ul><p><strong>Вывод:</strong>если стоит задача сэкономить на операциях записи, то Selection Sort - лучший выбор.</p>
100 </ul><p><strong>Вывод:</strong>если стоит задача сэкономить на операциях записи, то Selection Sort - лучший выбор.</p>
101 <p>Что касается памяти, то все три алгоритма в этом одинаково хороши. Они работают in-place и поэтому съедают<em>O(1)</em>памяти.</p>
101 <p>Что касается памяти, то все три алгоритма в этом одинаково хороши. Они работают in-place и поэтому съедают<em>O(1)</em>памяти.</p>
102 <p>Отмечу, что алгоритмы сортировки можно улучшать. В основном с помощью особых структур данных. Например, Insertion Sort можно ускорить примерно в два раза, а также свести количество свопов к показателям Bubble Sort, если использовать не массив, а связный список. А Selection Sort можно сделать стабильным.</p>
102 <p>Отмечу, что алгоритмы сортировки можно улучшать. В основном с помощью особых структур данных. Например, Insertion Sort можно ускорить примерно в два раза, а также свести количество свопов к показателям Bubble Sort, если использовать не массив, а связный список. А Selection Sort можно сделать стабильным.</p>
103 <p>Код сортировок на разных языках программирования и видео с визуализацией можно найти на сайте<a>geeksforgeeks.org</a>:</p>
103 <p>Код сортировок на разных языках программирования и видео с визуализацией можно найти на сайте<a>geeksforgeeks.org</a>:</p>
104 <ul><li><a>Bubble Sort</a></li>
104 <ul><li><a>Bubble Sort</a></li>
105 <li><a>Selection Sort</a></li>
105 <li><a>Selection Sort</a></li>
106 <li><a>Insertion Sort</a></li>
106 <li><a>Insertion Sort</a></li>
107 </ul><p>На YouTube полно визуализаций алгоритмов сортировки, но я выделю это видео:</p>
107 </ul><p>На YouTube полно визуализаций алгоритмов сортировки, но я выделю это видео:</p>
108 <p>Отдельный мем в этой теме - венгерские танцы. Выглядит не только познавательно, но и очень забавно:)</p>
108 <p>Отдельный мем в этой теме - венгерские танцы. Выглядит не только познавательно, но и очень забавно:)</p>
109 <p>Кажется, на этом тема простых сортировок исчерпана. Надеюсь, удалось всё разложить по полочкам и никого не запутать. В следующих статьях об алгоритмах разберу Merge Sort и Quick Sort и постараюсь залезть в материал поглубже.</p>
109 <p>Кажется, на этом тема простых сортировок исчерпана. Надеюсь, удалось всё разложить по полочкам и никого не запутать. В следующих статьях об алгоритмах разберу Merge Sort и Quick Sort и постараюсь залезть в материал поглубже.</p>
110 <a>Научитесь: Ал­го­рит­мы и струк­ту­ры дан­ных для раз­ра­бот­чи­ков Узнать больше</a>
110 <a>Научитесь: Ал­го­рит­мы и струк­ту­ры дан­ных для раз­ра­бот­чи­ков Узнать больше</a>