Паттерны быстрого поиска

Базовый Algorithms
≈ 2 мин чтения

Храни то, что понадобится искать снова

Один из главных алгоритмических паттернов с хеш-таблицей звучит так: если во время прохода приходится снова искать что-то среди уже просмотренных данных, попробуй сохранить эти данные в Set или Map. Дополнительная память часто превращает повторный линейный поиск в ожидаемую проверку за O(1).

Выбор структуры зависит от вопроса. Если достаточно знать, встречалось ли значение, подходит Set. Если вместе с ним нужно помнить индекс, счётчик или другую информацию — нужна Map.

Пример: Two Sum за один проход

Пусть нужно найти индексы двух элементов массива, сумма которых равна target. Для текущего числа numbers[i] можно сразу вычислить недостающее значение target - numbers[i] и проверить, встречалось ли оно раньше.

static int[] twoSum(int[] numbers, int target) {
    Map<Integer, Integer> indexByValue = new HashMap<>();

    for (int i = 0; i < numbers.length; i++) {
        int needed = target - numbers[i];
        Integer previousIndex = indexByValue.get(needed);

        if (previousIndex != null) {
            return new int[]{previousIndex, i};
        }

        indexByValue.put(numbers[i], i);
    }

    return new int[0];
}

Карта хранит значение элемента и индекс, на котором оно было встречено. Для каждого нового элемента выполняется одна ожидаемо быстрая проверка вместо просмотра всей предыдущей части массива. Поэтому ожидаемая временная сложность — O(n), а дополнительная память — O(n).

Почему сначала поиск, потом вставка

Текущий элемент добавляется в карту только после проверки needed. Так один и тот же индекс нельзя случайно использовать дважды. При этом два одинаковых значения на разных позициях обрабатываются корректно: второе увидит первое, если именно такая пара даёт нужную сумму.

Как узнавать этот паттерн

  • нужно быстро проверять, встречалось ли значение раньше;
  • нужно найти «дополняющее» значение вместо перебора всех пар;
  • нужно связать объект с уже вычисленной информацией и быстро получить её позже;
  • повторный поиск по уже обработанной части делает решение квадратичным.

Хеш-таблица не делает любой алгоритм быстрее автоматически. Мы выигрываем тогда, когда осознанно меняем часть времени на дополнительную память и можем заменить повторяющийся поиск быстрым обращением по ключу.

Всегда учитывай ожидаемую, а не гарантированную сложность. Для HashMap и HashSet обычно говорят об ожидаемом O(1) на операцию; худший случай зависит от распределения ключей и реализации структуры.