Услуги Портфолио Контакты БлогНовости +7 (800) 511‑32‑27 Обсудить проект
Статьи

Java сортировки: эффективные алгоритмы

21 августа 2023·4 мин чтения
Логотип YuSMP Group
Автор материалаРедакция компании YuSMP GroupБлог YuSMP Group
Профиль редакции
Java сортировки: эффективные алгоритмы
TL;DR — кратко о главном
  • QuickSort и MergeSort — самые практичные алгоритмы сортировки в Java, у обоих средняя сложность O(n log n).
  • QuickSort быстрее на практике и не требует лишней памяти, но нестабилен и может деградировать до O(n²); MergeSort стабилен и предсказуем, но тратит O(n) дополнительной памяти.
  • В прикладном коде почти всегда достаточно встроенных Arrays.sort() / Collections.sort() — под капотом Java сама выбирает Dual-Pivot Quicksort для примитивов и TimSort для объектов.
  • Ниже — таблица сравнения сложности, рабочий код на Java для QuickSort и MergeSort и чек-лист выбора алгоритма под конкретную задачу.

Сортировка – это одна из фундаментальных операций в программировании и компьютерных науках. Она позволяет упорядочивать структуры данных, что критично для производительности бэкенда: от индексов в базе данных до пагинации в API. В языке программирования представлены разнообразные методы и алгоритмы сортировки java, каждый из которых обладает своими уникальными характеристиками и применим в различных ситуациях. Мы в YuSMP Group закладываем выбор алгоритмов и структур данных уже на этапе разработки backend и API на Java — от этого решения напрямую зависит скорость отклика сервиса под нагрузкой.

Методы сортировки Java

Java предоставляет богатый набор инструментов для сортировки массивов java. Основные методы, предоставляемые в стандартной библиотеке, включают такие виды сортировок java:

  • Сортировка пузырьком java (BubbleSort) — простой способ, который последовательно сравнивает и меняет местами соседние элементы, пока весь массив не будет отсортирован. Сложность O(n²), из-за неэффективности редко используется в продакшене.
  • Сортировка вставками java (InsertionSort) — строит отсортированную последовательность по элементу за раз, вставляя каждый на правильное место в уже готовой части массива. Сложность O(n²), но на почти отсортированных данных приближается к O(n).
  • Сортировка выбором java (SelectionSort) — на каждом шаге находит минимальный элемент и меняет его местами с элементом на текущей позиции. Сложность всегда O(n²), независимо от исходного порядка данных.
  • Сортировка слиянием java (MergeSort) — разделяет массив на две части, рекурсивно сортирует каждую половину и затем сливает их в отсортированную последовательность. Гарантирует O(n log n) в любом случае и остаётся стабильной.
  • Быстрая сортировка java (QuickSort) — основана на принципе «разделяй и властвуй»: находит опорный элемент, делит массив на меньшие и большие относительно него части и рекурсивно обрабатывает обе. В среднем работает за O(n log n) и обычно быстрее MergeSort на практике.

Из всех перечисленных выше QuickSort и MergeSort считаются наиболее эффективными для реальных задач. Далее подробно разберём оба алгоритма, приведём рабочий код на Java и сравним их с остальными по сложности.

Сложность алгоритмов сортировки: таблица сравнения

Прежде чем выбирать конкретный способ, полезно увидеть все пять алгоритмов рядом — по времени выполнения в лучшем, среднем и худшем случае, по расходу памяти и по стабильности (сохранению порядка равных элементов):

АлгоритмЛучший случайСредний случайХудший случайПамятьСтабилен
BubbleSortO(n)O(n²)O(n²)O(1)Да
InsertionSortO(n)O(n²)O(n²)O(1)Да
SelectionSortO(n²)O(n²)O(n²)O(1)Нет
MergeSortO(n log n)O(n log n)O(n log n)O(n)Да
QuickSortO(n log n)O(n log n)O(n²)O(log n)Нет

Вывод из таблицы: у QuickSort самая низкая накладная память — сортировка идёт на месте, — но без оптимизации выбора опорного элемента он может деградировать до O(n²) на уже отсортированных или специально подобранных данных. MergeSort даёт гарантию O(n log n) в любом случае и сохраняет стабильность ценой O(n) дополнительной памяти под слияние.

Абстрактная визуализация принципа «разделяй и властвуй»: массив данных делится на части и сливается обратно в отсортированном порядке
Backend-разработка на Java
Узнайте цену

QuickSort — быстрая сортировка

Это один из наиболее широко используемых алгоритмов на этом и других языках программирования. Его особенность в том, что он имеет среднюю сложность O(n log n), что делает его подходящим для big data, и при этом сортирует массив на месте — без выделения дополнительной памяти под копии.

Принцип работы состоит в нахождении опорного элемента и разделении массива на две части — меньше опорного и больше опорного. Затем каждая из этих частей обрабатывается рекурсивно, пока не останутся отсортированные подмассивы из одного элемента.

public static void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        int pivotIndex = partition(arr, low, high);
        quickSort(arr, low, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, high);
    }
}

private static int partition(int[] arr, int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;
        }
    }
    int temp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = temp;
    return i + 1;
}

Оптимизация: как выбрать опорный элемент

Худший случай O(n²) у QuickSort возникает, когда опорный элемент раз за разом оказывается минимумом или максимумом подмассива — например, если всегда брать последний элемент на уже отсортированном массиве. На практике это лечат тремя способами:

  • Медиана трёх — в качестве опорного берут медиану первого, среднего и последнего элементов подмассива, а не фиксированную позицию.
  • Случайный выбор — опорный элемент выбирается случайно, что делает злонамеренный худший случай статистически маловероятным.
  • Переход на InsertionSort — на маленьких подмассивах (обычно меньше 10–20 элементов) рекурсию QuickSort обрывают и досортировывают вставками: на таком размере она быстрее из-за меньших накладных расходов.

Именно комбинацию этих приёмов использует Dual-Pivot Quicksort — реализация, которая лежит в основе Arrays.sort() для примитивных типов в самой Java (подробнее — в разделе про встроенные методы ниже).

MergeSort — сортировка слиянием

MergeSort обеспечивает стабильную производительность даже на больших объёмах данных: в отличие от QuickSort, у него нет плохого случая — сложность всегда O(n log n), вне зависимости от исходного порядка элементов. Основной принцип — разделить массив пополам, рекурсивно отсортировать каждую половину и затем слить два отсортированных подмассива в один.

public static void mergeSort(int[] arr, int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2;
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}

private static void merge(int[] arr, int left, int mid, int right) {
    int[] leftArr = Arrays.copyOfRange(arr, left, mid + 1);
    int[] rightArr = Arrays.copyOfRange(arr, mid + 1, right + 1);
    int i = 0, j = 0, k = left;
    while (i < leftArr.length && j < rightArr.length) {
        arr[k++] = (leftArr[i] <= rightArr[j]) ? leftArr[i++] : rightArr[j++];
    }
    while (i < leftArr.length) arr[k++] = leftArr[i++];
    while (j < rightArr.length) arr[k++] = rightArr[j++];
}

Ключевое свойство MergeSort — стабильность: элементы с равными ключами сохраняют исходный взаимный порядок, потому что на шаге слияния при равенстве всегда сначала берётся элемент из левой половины (leftArr[i] <= rightArr[j]). Это важно, когда сортировка выполняется по одному полю, а порядок по другому полю нужно сохранить — например, сортировка заказов по статусу с сохранением исходной сортировки по дате.

Плата за эту стабильность и гарантию — память: слияние требует O(n) дополнительного места под временные массивы leftArr и rightArr, а не сортирует на месте, как QuickSort.

Какой алгоритм сортировки использует Java «из коробки»?

В реальных проектах писать QuickSort или MergeSort руками почти никогда не нужно — в стандартной библиотеке уже есть проверенные, оптимизированные реализации:

  • Arrays.sort(int[]) и другие перегрузки для примитивных типов используют Dual-Pivot Quicksort — модификацию QuickSort с двумя опорными элементами вместо одного, которая на практике быстрее классического QuickSort и MergeSort для примитивов.
  • Arrays.sort(Object[]) и Collections.sort(List) для объектов используют TimSort — гибридный алгоритм на основе MergeSort и InsertionSort, изначально разработанный для Python и портированный в Java. TimSort стабилен (это критично для объектов, где вместе с ключом сортировки переносятся другие поля) и хорошо использует уже упорядоченные участки данных («natural runs»).
  • Для сортировки по кастомному критерию используют Comparator: list.sort(Comparator.comparing(User::getRegisteredAt)) или цепочку .thenComparing() для сортировки по нескольким полям.

Почему для примитивов и объектов выбраны разные алгоритмы? Примитивы сравнивать дёшево, и стабильность неважна — нет привязанных данных, которые нужно сохранить в исходном порядке. Для объектов сравнение дороже, а стабильность часто критична, поэтому Java жертвует небольшим выигрышем в скорости ради гарантированной стабильности TimSort.

Как выбрать алгоритм сортировки для своей задачи?

Выбор подходящего способа зависит от объёма данных, доступных ресурсов и требований к стабильности:

  • Маленький массив (до ~20 элементов): InsertionSort или даже BubbleSort — из-за простоты и минимальной дополнительной нагрузки они на таком размере не медленнее QuickSort или MergeSort.
  • Большой объём данных: QuickSort или MergeSort — оба дают O(n log n) там, где InsertionSort и BubbleSort скатываются в O(n²).
  • Нужна стабильность: MergeSort или TimSort (встроенная сортировка объектов в Java) — QuickSort порядок равных элементов не сохраняет.
  • Ограничена память: QuickSort — сортирует на месте и не требует O(n) под временные массивы, в отличие от MergeSort.
  • Данные почти отсортированы: InsertionSort или TimSort — оба эффективно используют уже упорядоченные участки и на таких данных приближаются к O(n).

На практике для собственного кода это редко значит «писать алгоритм с нуля» — почти всегда достаточно вызвать Arrays.sort() или Collections.sort() и, если нужно, передать свой Comparator. Знание внутреннего устройства этих методов помогает объяснить, почему сортировка объектов ведёт себя иначе, чем сортировка примитивов, и когда стоит задуматься об оптимизации вручную — например, при сортировке миллионов записей в потоковой обработке.

Абстрактная визуализация ветвящегося дерева решений при выборе алгоритма сортировки

Частые вопросы о сортировке в Java

Какой алгоритм сортировки самый быстрый в Java?

На практике — Dual-Pivot Quicksort, который используется под капотом Arrays.sort() для примитивных типов: он быстрее классического QuickSort и MergeSort на большинстве реальных наборов данных. В теории у QuickSort, MergeSort и Dual-Pivot Quicksort одинаковая средняя сложность O(n log n) — разница в константах и работе с кэшем процессора.

Почему QuickSort быстрее MergeSort, если у обоих одинаковая сложность O(n log n)?

Дело не в теоретической сложности, а в константах и работе с памятью. QuickSort сортирует на месте и лучше использует кэш процессора, а MergeSort постоянно выделяет и копирует временные массивы. На практике это даёт QuickSort ощутимое преимущество, хотя у него есть риск худшего случая O(n²), которого у MergeSort нет.

Какая сортировка используется в Java по умолчанию?

Arrays.sort() для массивов примитивов (int, long, double и др.) использует Dual-Pivot Quicksort. Для массивов объектов и списков (Arrays.sort(Object[]), Collections.sort()) используется TimSort — стабильный гибрид MergeSort и InsertionSort.

Что такое стабильная сортировка и когда это важно?

Стабильная сортировка сохраняет исходный относительный порядок элементов с одинаковыми ключами. Это важно, когда список нужно отсортировать сразу по нескольким критериям: например, список уже отсортирован по дате, а затем его стабильно сортируют по статусу — записи с одинаковым статусом останутся в порядке по дате.

Можно ли отсортировать список объектов по нескольким полям сразу?

Да, через цепочку компараторов: list.sort(Comparator.comparing(Order::getStatus).thenComparing(Order::getCreatedAt)). Такой код сначала сортирует по статусу, а внутри одинакового статуса — по дате создания, и не требует писать собственный алгоритм сортировки.

Какая сложность у быстрой сортировки в худшем случае?

O(n²) — это происходит, когда опорный элемент раз за разом оказывается минимальным или максимальным в подмассиве, например на уже отсортированных данных при наивном выборе опорного (последний элемент). На практике этого избегают выбором медианы трёх или случайного опорного элемента — тогда худший случай становится статистически маловероятным.

Быстрая и слиянием остаются самыми универсальными алгоритмами сортировки Java для промышленной разработки, а для повседневных задач чаще хватает встроенных Arrays.sort() и Collections.sort(). Понимание того, что происходит под капотом, помогает разработчикам осознанно выбирать между готовыми методами и ручной реализацией, когда стандартной производительности не хватает.
Этот материал подготовили для вас специалисты веб-услуг и разработки в YuSMP Group. Больше интересных текстов по технологиям и ИТ в блоге студии web-разработки YuSMP Group, а в разделе кейсы можно найти реальные проекты, которые мы создали.

Нужны разработчики в команду?

Подберём инженеров нужного стека под ваш проект — быстрый старт.

Обсудить команду