SRT Поиск и сортировка Алгоритм
Простые сортировки
Также: Simple sorts · сортировка вставками · сортировка выбором · сортировка пузырьком · insertion sort · selection sort · bubble sort
Три способа, которыми человек раскладывает вещи по порядку: найти наименьшее, вставить на место, поменять соседей. На случайных данных все три тратят порядка n² действий, но характеры у них разные: вставки мгновенно досортировывают почти готовое и потому живут внутри быстрых библиотечных сортировок, выбор переставляет меньше всех, а пузырёк уступает обоим.
В каталоге USGS 462 землетрясения силой от 6,5 за 2015–2025 годы. Каталог хранит их по времени, а нам нужна таблица по силе, от слабых к сильным. Вручную такой список упорядочивают тремя способами, и все три знакомы с детства: найти самое слабое и положить первым; брать карточки по одной и вставлять каждую на своё место; менять местами соседей, пока все не встанут как надо. Ниже — все три на настоящем каталоге, бок о бок. Полоска — землетрясение, высота — сила, цвет — дата. Переключите набор и порядок на входе: «таблица + 2025» — это отсортированная таблица прошлых лет, в конец которой дописали события нового года.
Выбор на 462 землетрясениях делает 106 491 сравнение при любом порядке входа — ровно столько, сколько пар можно составить из 462 элементов. Вставкам на исходный порядок нужно 47 801 сравнение, а на «таблицу + 2025» — всего 8985: каждое новое землетрясение проезжает влево только до своего места. Пузырёк на той же таблице не сэкономил ни одного сравнения: 103 488, столько же, сколько на исходном порядке. Почему так выходит, видно из того, как они устроены. А когда все трое закончат, присмотритесь к цвету внутри каждой ступеньки одинаковой силы.
Идея
Все три алгоритма растят отсортированную часть массива по одному элементу за проход. Различаются они тем, какой элемент добавить и как его довезти: выбор находит наименьший из оставшихся и ставит его одним дальним обменом, вставки берут следующий по очереди и ведут его влево мимо больших, пузырёк меняет местами соседей, и наибольший сам выезжает в конец.
Что именно растёт, у каждого своё, и от этого зависит всё остальное:
| Алгоритм | Что верно после i проходов | Сравнений | Перестановок |
|---|---|---|---|
| выбор | первые i мест заняты i наименьшими, и это их окончательные места | всегда $n(n-1)/2$ | не больше $n-1$ обмена |
| вставки | первые i + 1 элементов упорядочены между собой, но на окончательных местах ещё не стоят | от $n-1$ до $n(n-1)/2$ | столько сдвигов, сколько инверсий |
| пузырёк | последние i мест заняты i наибольшими, окончательно | от $n-1$ до $n(n-1)/2$ | столько обменов, сколько инверсий |
Как работает каждый
Выбор. Просматриваем все оставшиеся элементы, запоминаем наименьший и меняем его местами с первым из оставшихся. Чтобы убедиться, что элемент наименьший, нужно сравнить его со всеми остальными, и никакая удача входа от этой работы не избавляет: на уже отсортированном массиве выбор тратит те же n(n − 1)/2 сравнений. Зато обмен на проход — один, а если наименьший уже стоит на месте, то ни одного.
Вставки. Так сортирует руку карточный игрок: берёт карты по одной и каждую сразу вставляет на место среди уже взятых. В массиве «вставить» значит сдвинуть вправо на одну позицию все большие элементы слева от нового и положить его в освободившуюся ячейку. Сдвиг останавливается, как только слева оказался элемент не больше нового. Если массив почти готов, остановка случается сразу, и работа почти линейная.
Пузырёк. Идём слева направо и меняем местами соседей, если левый больше. Наибольший элемент, раз попавшись, едет вправо до самого конца — за один проход. Если за проход не случилось ни одного обмена, массив отсортирован и можно остановиться. У пузырька заметная асимметрия: большой элемент в начале добирается до конца за один проход, а маленький в конце сдвигается влево лишь на одну позицию за проход. В «таблице + 2025» новые землетрясения стоят в самом конце, самым слабым из них ехать почти через весь массив, и пузырьку нужно почти столько же проходов, сколько элементов. Вставки берут такой элемент и проводят до места за один раз. Пузырёк можно подлечить, чередуя направление проходов (так называемая шейкерная сортировка), но квадрат никуда не денется.
Почему квадрат: инверсии
Мерило беспорядка в массиве — инверсии: пары, в которых больший элемент стоит левее меньшего. В отсортированном массиве их нет, в развёрнутом — все n(n − 1)/2 пар. У 462 землетрясений в порядке времени инверсий 47 340, у «таблицы + 2025» — 8524.
Алгоритм, который переставляет только соседние элементы, делает не меньше I перестановок, где I — число инверсий во входе. Сортировка вставками и пузырёк делают ровно I.
Обмен соседей a[k] и a[k + 1] не меняет взаимного порядка ни одной пары, кроме этих двоих: все остальные элементы как стояли левее или правее каждого из них, так и стоят. Значит, один обмен меняет число инверсий не больше чем на единицу. В начале их I, в конце ноль — обменов не меньше I.
Вставки и пузырёк меняют местами соседей, только когда левый строго больше правого, то есть только инверсную пару, и каждый такой обмен уменьшает I ровно на единицу. Поэтому их обменов (у вставок — сдвигов) ровно I.
Сколько инверсий в случайном массиве? Возьмём различные элементы в случайном порядке. Если развернуть массив задом наперёд, каждая пара, бывшая инверсией, перестанет ею быть, и наоборот. Перестановка и её разворот равновероятны, поэтому каждая из n(n − 1)/2 пар оказывается инверсией с вероятностью ровно 1/2, и в среднем инверсий n(n − 1)/4. Отсюда приговор всем алгоритмам, которые двигают элементы на одну позицию за раз: на случайных данных им нужно порядка n² перестановок, как ни старайся.
Посмотрите на это вживую. Ниже 45 землетрясений силой от 6,5 за 2025 год; каждая клетка треугольника — пара, которая стоит не в том порядке. Синие клетки исчезают по одной на каждый обмен вставок и пузырька — и ни одной больше.
Переключите на выбор. Его длинный обмен разворачивает сразу много пар: элемент перелетает через весь массив и меняет порядок со всеми, кого миновал. Иногда при этом появляются и новые инверсии, а оранжевые клетки — это пары равной силы, которые выбор переставил по пути. Чтобы быстро сортировать, нужно двигать элементы далеко — это и делают сортировка слиянием, быстрая и пирамидальная. Самый короткий мостик от вставок к ним придумал Дональд Шелл в 1959 году: сначала делать вставки с большим шагом, через h позиций, и уменьшать шаг до единицы.
Из теоремы следует и сила вставок. Каждое сравнение у них либо сдвигает элемент, убирая одну инверсию, либо останавливает его, а остановок не больше n − 1. Поэтому сравнений не больше I + n − 1, и время — O(n + I). Сортировку, которая работает тем быстрее, чем ближе вход к отсортированному, называют адаптивной. На «таблице + 2025» инверсий 8524 — и сравнений 8985.
Устойчивость: что бывает с равными
Магнитуды в каталоге записаны в основном с одним знаком после запятой, и у 462 землетрясений всего 20 разных значений: почти у каждого есть ровесники по силе. Сортировка по силе ничего не говорит о том, в каком порядке должны идти равные, — и алгоритмы ведут себя по-разному. Сортировку называют устойчивой, если равные элементы выходят из неё в том же порядке, в каком вошли.
Вставки и пузырёк устойчивы. Вставки останавливают новый элемент перед первым, кто не больше его, и равный элемент никогда не обгоняет равного; пузырёк меняет соседей, только если левый строго больше. В первой сцене это видно по цвету: внутри каждой ступеньки одинаковой силы цвет идёт от тёмного к светлому, от 2015 года к 2025-му. У выбора цвета перемешаны: его дальний обмен уносит элемент через головы равных ему. На 462 землетрясениях выбор переставил 2284 пары равной силы. Вот то же на двадцати землетрясениях 2025 года силой от 6,9 — даты внутри групп равной силы после сортировки выбором:
selection: 190 comparisons, 18 swaps 6.9: 04-03 04-04 07-30 09-30 7.0: 07-28 03-30 12-06 <- out of date order 7.4: 07-20 09-13 10-10 05-02 <- out of date order 7.6: 02-08 10-10 12-08
Зачем это нужно: сортировка по нескольким ключам. Отсортируйте таблицу по дате, потом устойчиво — по силе, и внутри одной силы записи останутся по дате, без составного ключа. Встроенная сортировка Python устойчива всегда. В Go обычные sort.Sort и slices.Sort устойчивость не обещают, а sort.Stable и slices.SortStableFunc — обещают. Устойчивость легко потерять по невнимательности: замените во вставках «строго меньше» на «не больше», и массив по-прежнему отсортируется, но равные станут обгонять друг друга, а сдвигов прибавится.
Код
Все три функции сортируют на месте и возвращают счётчики: сравнения и перестановки. В Python сравнение идёт по ключу, как у встроенной sorted; в Go — через функцию «меньше» и обобщённый тип, как в пакете slices. Демо — двадцать землетрясений 2025 года силой от 6,9.
Обратите внимание на вставки: новый элемент не меняется местами с каждым соседом, а запоминается в x, большие соседи сдвигаются на его место, и только в конце он записывается в освободившуюся ячейку. Это вдвое меньше записей в память, чем обмены, — одна из причин, почему вставки на практике быстрее пузырька при том же числе перестановок.
А это первая сцена в виде программы: все 462 землетрясения силой от 6,5 из каталога на сервере, три порядка на входе, три алгоритма. Цифры совпадают с теми, что показывала сцена.
Сколько это стоит
Памяти все трое берут O(1): пара индексов и один элемент «в руке». Время считаем в сравнениях и перестановках, для n различных элементов:
| Лучший случай | В среднем | Худший случай | |
|---|---|---|---|
| выбор: сравнения | $n(n-1)/2$ всегда | ||
| выбор: обмены | 0 | около n | $n-1$ |
| вставки: сравнения | $n-1$ | около $n^2/4$ | $n(n-1)/2$ |
| вставки: сдвиги | 0 | $n(n-1)/4$ | $n(n-1)/2$ |
| пузырёк: сравнения | $n-1$ | около $n^2/2$ | $n(n-1)/2$ |
| пузырёк: обмены | 0 | $n(n-1)/4$ | $n(n-1)/2$ |
Лучший случай вставок и пузырька — уже отсортированный массив, худший — развёрнутый. Ниже замер на случайных числах, на Python и на Go. Оси логарифмические, и наклон прямой — показатель степени: у трёх квадратичных он близок к 2, у вставок на почти готовой таблице (отсортированные числа и десять новых в конце) — к 1. Для сравнения — встроенная сортировка на случайных данных.
Пузырёк медленнее всех и на Python, и на Go: сравнений у него вдвое больше, чем у вставок, а каждый обмен — две записи в память. Вставки на Go почти втрое быстрее выбора, ведь сравнений у них в среднем вдвое меньше. На Python же они почти сравнялись: интерпретатор тратит на каждый шаг цикла while со сдвигом больше, чем на шаг цикла for у выбора, и выигрыш в числе сравнений съедается. Это хороший повод мерить, а не считать на бумаге. И ещё одно: на Python встроенная сортировка шестидесяти четырёх тысяч чисел заканчивается раньше, чем любая из квадратичных справляется с полутора тысячами.
Где они работают
Вставки — внутри быстрых сортировок. Сортировка слиянием и быстрая сортировка хороши на больших массивах, но на кусках из десятка элементов их накладные расходы — вызовы, разбиения, буферы — больше самой работы. Поэтому многие стандартные библиотеки добивают маленькие куски вставками. Timsort в Python сортирует список короче 64 элементов целиком двоичными вставками (место ищут двоичным поиском), а короткие упорядоченные куски длинных списков вставками же доращивает до 32–64 элементов. Сортировка в Go переходит на вставки на кусках до 12 элементов, std::sort в libstdc++ — до 16, Arrays.sort для массивов чисел в Java — до 44. У вставок нет рекурсии, они читают память подряд и почти ничего не делают на почти готовых данных.
Вставки — когда данные приходят по одному. «Таблица + 2025» — ровно этот случай: отсортированный список и несколько новых записей. Вставка одной записи — один проход влево. Для больших таблиц, куда записи идут потоком, лучше дерево поиска или куча, но для сотни-другой строк вставки проще и быстрее всего.
Выбор — когда перестановка дорога. Выбор делает не больше n − 1 обменов, а на различных элементах — ровно столько, сколько необходимо: каждый его обмен ставит на окончательное место хотя бы один элемент (почему ровно столько и не меньше — в задаче о коробках в архиве). Это важно, когда переставлять гораздо дороже, чем сравнивать, — тяжёлые записи, медленная запись в память. А если искать минимум не просмотром, а кучей, выбор превращается в пирамидальную сортировку за O(n log n).
Пузырёк в настоящем коде почти не встречается: он делает столько же перестановок, сколько вставки, но больше сравнений и вдвое больше записей. Дональд Кнут в третьем томе «Искусства программирования» заметил, что достоинств у него, кажется, нет, кроме запоминающегося названия и пары интересных теоретических задач. Его место — в учебниках, как самая короткая программа, которая сортирует.
Откуда имена
В 1956 году Эдвард Френд в журнале JACM разобрал сортировку на электронных машинах и сравнил вставки, выбор и обмены. Об обменах он писал, что им нужно по меньшей мере вдвое больше сравнений, чем вставкам, и на большинстве машин они будут хуже. Слова «пузырёк» там ещё нет: его, по поискам Оуэна Астракана (2003), первым напечатал Кеннет Айверсон в книге «A Programming Language» 1962 года. До того алгоритм звали «сортировкой обменами». В 1963 году он попал в сборник алгоритмов ACM как Algorithm 175 под именем Shuttle Sort — и вскоре в опубликованном коде нашли ошибки.
А в 1959 году Тони Хоар, тогда университетский стажёр, работал над машинным переводом. Русско-английский словарь хранился на магнитной ленте по алфавиту, и слова предложения выгодно было сначала отсортировать, чтобы найти их все за один проход ленты. По воспоминаниям Хоара, первым ему пришёл в голову тот способ, который теперь называют пузырьком, — и он отверг его как слишком медленный. Вторым была быстрая сортировка.
Задачи
Две задачи о наименьшем числе перестановок с разными правилами. В первой менять можно только соседей, и ответ даёт теорема об инверсиях. Во второй — любые два элемента, и ответ совсем другой.
Картотека в лотке Коробки в архивеДанные: Землетрясения: каталог USGS, общественное достояние