Алгоритмы на Java: поиск, сортировка и сложность с примерами кода

Алгоритмы на Java: поиск, сортировка и сложность с примерами кода Полезное

Алгоритм — это конечная последовательность точно определенных шагов, которая по входным данным за конечное число операций дает результат. В Java алгоритм — это обычный метод: он принимает данные (массив, список, строку) и возвращает ответ или меняет структуру на месте.

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

Как измеряют сложность алгоритма

Прежде чем сравнивать алгоритмы, нужен язык оценки скорости. Его дает O-нотация: она описывает, как растет число операций при увеличении объема данных n, отбрасывая константы.

Считают не секунды (они зависят от машины), а порядок роста. O(1) — время не зависит от n, O(log n) — растет очень медленно, O(n) — линейно, O(n log n) — чуть медленнее линейного, O(n^2) — квадратично и болезненно на больших данных.

Алгоритм Сложность (в среднем) Условие
Линейный поиск O(n) массив в любом порядке
Двоичный поиск O(log n) массив отсортирован
Сортировка выбором O(n^2) учебная реализация
Arrays.sort для примитивов O(n log n) двухопорный quicksort
Arrays.sort для объектов O(n log n) TimSort, устойчивая

Разница O(n) и O(log n) видна на числах: чтобы найти элемент в миллионе значений, линейный поиск в худшем случае сделает около миллиона сравнений, а двоичный — около 20.

Линейный поиск: простой и медленный

Линейный поиск проходит массив с начала и сравнивает каждый элемент с искомым, пока не найдет совпадение или не дойдет до конца. Он не требует сортировки, но в худшем случае перебирает все n элементов, поэтому его сложность — O(n).

public class LinearSearch {
    static int indexOf(int[] a, int target) {
        for (int i = 0; i < a.length; i++) {
            if (a[i] == target) {
                return i;          // нашли - возвращаем позицию
            }
        }
        return -1;                 // дошли до конца - элемента нет
    }

    public static void main(String[] args) {
        int[] data = {7, 3, 9, 1, 5, 8};
        System.out.println("индекс 5: " + indexOf(data, 5));
        System.out.println("индекс 4: " + indexOf(data, 4));
    }
}

Вывод программы:

индекс 5: 4
индекс 4: -1

Значение 5 стоит на позиции 4 (индексы в Java считаются с нуля), поэтому метод вернул 4. Числа 4 в массиве нет — вернулся признак «не найдено» -1. Линейный поиск берут, когда данных мало или они не отсортированы и сортировать их ради одного поиска невыгодно.

Двоичный поиск: делим пополам

Двоичный поиск работает только по отсортированному массиву. Он смотрит средний элемент и за одно сравнение отбрасывает половину диапазона: если искомое меньше среднего — продолжает слева, если больше — справа. За счет этого сложность падает до O(log n).

Проследим поиск числа 9 в массиве {1, 3, 4, 6, 7, 9, 11, 15} (индексы 0-7):

  • Границы 0 и 7, середина — индекс 3, там 6. 9 больше 6 — ищем правее, границы 4 и 7.
  • Границы 4 и 7, середина — индекс 5, там 9. Совпадение — ответ 5.

Тот же алгоритм в коде, итеративный вариант:

public class BinarySearch {
    static int search(int[] a, int target) {
        int low = 0, high = a.length - 1;
        while (low <= high) {
            int mid = low + (high - low) / 2;   // защита от переполнения
            if (a[mid] == target) {
                return mid;
            } else if (a[mid] < target) {
                low = mid + 1;                  // цель правее середины
            } else {
                high = mid - 1;                 // цель левее середины
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        int[] sorted = {1, 3, 4, 6, 7, 9, 11, 15};
        System.out.println("индекс 9: " + search(sorted, 9));
        System.out.println("индекс 6: " + search(sorted, 6));
        System.out.println("индекс 2: " + search(sorted, 2));
    }
}

Вывод программы:

индекс 9: 5
индекс 6: 3
индекс 2: -1

Середину считают как low + (high - low) / 2, а не (low + high) / 2: на больших границах сумма low + high может выйти за пределы int и стать отрицательной, а первый вариант этого избегает. Важна граница применимости: если массив не отсортирован, двоичный поиск вернет неверный результат, а не ошибку, — сортировку надо гарантировать заранее.

Сортировка выбором

Сортировка выбором на каждом шаге находит минимум в неотсортированной части массива и ставит его в начало этой части. Реализация наглядная, но делает вложенные проходы, поэтому ее сложность — O(n^2).

Проследим сортировку массива {5, 1, 4, 2, 8} по шагам (жирным — минимум, который встает на место):

  • Минимум всего массива — 1, меняем с первым: {1, 5, 4, 2, 8}.
  • Минимум остатка {5, 4, 2, 8} — это 2, меняем с 5: {1, 2, 4, 5, 8}.
  • Минимум остатка {4, 5, 8} — это 4, уже на месте: {1, 2, 4, 5, 8}.
  • Остается {5, 8}, оба на местах — массив отсортирован.
import java.util.Arrays;

public class SelectionSort {
    static void sort(int[] a) {
        for (int i = 0; i < a.length - 1; i++) {
            int min = i;
            for (int j = i + 1; j < a.length; j++) {
                if (a[j] < a[min]) {
                    min = j;                 // запомнили индекс меньшего
                }
            }
            int tmp = a[min];                // обмен a[i] и a[min]
            a[min] = a[i];
            a[i] = tmp;
        }
    }

    public static void main(String[] args) {
        int[] data = {5, 1, 4, 2, 8};
        sort(data);
        System.out.println(Arrays.toString(data));
    }
}

Вывод программы:

[1, 2, 4, 5, 8]

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

Готовые алгоритмы в стандартной библиотеке Java

Поиск и сортировку в Java почти всегда решают штатными методами. Класс java.util.Arrays дает sort и binarySearch для массивов, а Collections.sort сортирует списки. Это быстрее самописных O(n^2)-вариантов и не содержит типичных ошибок с границами.

import java.util.Arrays;

public class StdLib {
    public static void main(String[] args) {
        int[] data = {5, 1, 4, 2, 8, 3};
        Arrays.sort(data);                       // сортировка на месте
        System.out.println("отсортировано: " + Arrays.toString(data));

        int pos = Arrays.binarySearch(data, 4);  // элемент есть
        System.out.println("индекс 4: " + pos);

        int miss = Arrays.binarySearch(data, 7); // элемента нет
        System.out.println("для 7: " + miss);
    }
}

Вывод программы:

отсортировано: [1, 2, 3, 4, 5, 8]
индекс 4: 3
для 7: -6

Здесь важная деталь Java, которая часто путает новичков: Arrays.binarySearch при отсутствии элемента возвращает не -1, а отрицательное число вида -(точка вставки) - 1. Для 7 точка вставки — индекс 5 (между 5 и 8), поэтому ответ -6. По этому числу можно понять, куда вставить элемент, чтобы массив остался отсортированным. И binarySearch тоже требует заранее отсортированный массив — иначе результат неопределен.

Жадный алгоритм на примере размена монет

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

public class GreedyCoins {
    static int[] coins = {10, 5, 2, 1};      // номиналы по убыванию

    static void change(int amount) {
        int rest = amount;
        for (int coin : coins) {
            int count = rest / coin;         // сколько таких монет влезает
            if (count > 0) {
                System.out.println(coin + " x " + count);
            }
            rest = rest % coin;              // остаток к размену
        }
    }

    public static void main(String[] args) {
        change(27);
    }
}

Вывод программы:

10 x 2
5 x 1
2 x 1

Для 27 при номиналах 10, 5, 2, 1 жадный алгоритм дает 4 монеты, и это оптимум. Но жадность работает не всегда: на номиналах 1, 3, 4 для суммы 6 жадный возьмет 4 + 1 + 1 (три монеты), а оптимум — 3 + 3 (две). Поэтому жадный подход применяют, когда доказано, что локальный выбор ведет к глобальному оптимуму; иначе берут динамическое программирование.

Выводы

  • Алгоритм в Java — это метод над данными; сравнивают алгоритмы по O-нотации, то есть по порядку роста числа операций от объема данных.
  • Линейный поиск O(n) не требует сортировки; двоичный поиск O(log n) в разы быстрее, но работает только по отсортированному массиву.
  • Сортировка выбором O(n^2) — учебная; в реальном коде берут Arrays.sort и Arrays.binarySearch, помня, что при промахе binarySearch возвращает -(точка вставки) - 1.
  • Жадный алгоритм прост и быстр, но дает оптимум не для любых входных данных — границу применимости нужно проверять.

Где применяется и что учить дальше

Поиск и сортировка — фундамент почти любой программы: выборка записей из коллекции, подготовка данных к двоичному поиску, обработка пользовательского ввода. Понимание O-нотации помогает заранее оценить, выдержит ли алгоритм рост данных, а не ловить тормоза уже в проде.

Освойте тему на практике

Чтобы освоить синтаксис Java и уверенно писать такие методы с нуля, посмотрите курс Java Basic в Otus — он рассчитан на старт с азов языка. Оценить формат и уровень до оплаты помогают бесплатные открытые уроки Otus — живые занятия с преподавателями.

Смежные темы: Scala: что нужно знать начинающим программистам, Криптография: что нужно знать про алгоритмы.

FAQ

Нужно ли писать поиск и сортировку вручную в реальных проектах? Обычно нет. Для сортировки берут Arrays.sort или Collections.sort, для поиска в отсортированном массиве — Arrays.binarySearch. Самописные реализации нужны учебно или когда требуется нестандартная логика сравнения.

Почему двоичный поиск не стоит применять к LinkedList? Двоичному поиску нужен быстрый доступ к элементу по индексу за O(1), как в массиве или ArrayList. У LinkedList доступ по индексу — O(n), поэтому выигрыш O(log n) теряется, и линейный проход оказывается не хуже.

Что означает отрицательный результат Arrays.binarySearch? Это не просто «не найдено»: метод возвращает -(точка вставки) - 1. Точка вставки — индекс, куда встал бы элемент, чтобы массив остался отсортированным. По этому числу удобно сразу вставлять элемент в нужную позицию.

OTUS Журнал
Бесплатные открытые уроки (поп-ап)