Где применяется
1. Задача
Проверить каждую пару элементов стоит O(n2), и урок про Big-O показал, как быстро это растёт. Когда данные отсортированы или тебе важны только соседние элементы, два подвижных маркера за один проход делают то, на что вложенным циклам нужно n2 шагов.
У тебя есть прайс-лист, уже отсортированный от дешёвого к дорогому, и подарочная карта на точную сумму. Нужно найти два товара, цены которых в сумме дают ровно эту сумму, чтобы потратить карту до копейки.
Первое, что приходит в голову: перебрать все пары. Товар 1 с товаром 2, товар 1 с товаром 3 и так далее. Для короткого списка это нормально. Для списка из миллиона цен это миллион на миллион пар, та самая ловушка O(n2) из урока 09.
Но список отсортирован, и это всё меняет. Благодаря порядку можно отбрасывать целые части списка, не проверяя их, и найти ответ за один проход.
2. Простыми словами
Возьми первый тест: цены [1, 3, 4, 6, 8] и нужная сумма 10. Поставь одну метку на самую дешёвую цену, 1, а другую на самую дорогую, 8. Вместе они дают 9, это мало.
1 не дотягивает до 10 даже вместе с 8, самой большой ценой. С любой ценой поменьше ей не хватит ещё больше. Значит, 1 не входит ни в один ответ, и её можно отбросить: левая метка переходит на 3. Теперь 3 + 8 = 11, это много. То же рассуждение с другой стороны: 8 слишком велика даже с самой маленькой из оставшихся цен, отбрасываем её, и правая метка переходит на 6. Дальше 3 + 6 = 9, снова мало, и левая метка переходит на 4. Наконец 4 + 6 = 10. Пара найдена.
Каждый сдвиг отбрасывал одну цену вместе со всеми парами, которые она могла составить, и ничего не проверялось дважды. Хватило четырёх сумм вместо всех десяти пар.
3. Идея
Поставь маркер lo на самый дешёвый товар, а маркер hi на самый дорогой. Сложи две цены, на которые они указывают.
- Сумма равна цели: пара найдена, готово.
- Сумма слишком мала: увеличить её можно, только взяв товар подороже вместо дешёвого. Сдвинь
loна одну позицию вправо. - Сумма слишком велика: сдвинь
hiна одну позицию влево, чтобы сумма уменьшилась.
Каждый шаг безопасен, потому что список отсортирован. Пусть сумма мала. Тогда nums[lo] не дотягивает до цели даже вместе с самой дорогой из оставшихся цен, nums[hi]. С остальными, более дешёвыми, он не дотянет тем более. Значит, nums[lo] не входит ни в одну подходящую пару, и его можно отбросить. Останавливайся, когда маркеры встретятся.
Проследим на [1, 3, 4, 6, 8] с целью 10. lo указывает на 1, hi на 8: сумма 9, мало, двигаем lo. lo на 3, hi на 8: сумма 11, много, двигаем hi. lo на 3, hi на 6: сумма 9, мало, двигаем lo. lo на 4, hi на 6: сумма 10, нашли. Ответ: позиции 2 и 3.
4. Во что это обходится
Время O(n): каждый маркер движется только внутрь, поэтому вместе они делают не больше n шагов. Память O(1): две переменные с индексами, и ничего не растёт вместе со входом. Если список сначала нужно отсортировать, добавь O(n log n) на сортировку.
Сравни с подходом на хеше из урока 18. Он тоже находит пару за O(n) по времени, но тратит O(n) памяти на множество. На уже отсортированных данных два указателя дают ту же скорость и не требуют лишней памяти.
5. Где это встречается
Сверка платежей: в журнале операций, отсортированном по сумме, ищут два исходных списания, которые вместе дают сумму возврата. Биржевой стакан (список заявок, упорядоченный по цене) сводит заявку на покупку с заявкой на продажу. Системы антифрода (поиска мошенничества) и удаления дубликатов ищут в отсортированных записях пару, которая отличается не больше чем на допустимую величину. Везде, где сопоставляют деньги или упорядоченные записи, задача выглядит одинаково: отсортированный вход и поиск пары. Решают её двумя указателями.
twoPointers([1, 2, 4, 6, 8, 11, 15], 14)lo
hi
Ищем два числа с суммой 14. Массив отсортирован, поэтому lo стоит на самом маленьком числе, а hi на самом большом.
Столбики: отсортированные числа. Ровно одна пара из них даёт нужную сумму.
Одна операция: одна проверенная пара. За каждый кадр все дорожки делают одинаковое число операций, поэтому медленная отстаёт.
O(n²)O(n)
Операции для n чисел и примерное время при 100 миллионах простых операций в секунду. Это оценка по тому же счёту, а не замер.
Как узнать
Эти слова в условии значат этот паттерн
- отсортированный массив (или можно отсортировать)
- найти пару/тройку с заданной суммой
- сжать с обоих концов (контейнер, площадь)
- проверка палиндрома
Похоже, но не то
- Скользящее окно (переменное) (Sliding window (variable))
Окно хранит живой отрезок, и оба конца идут вперёд. Эти указатели стартуют врозь и сходятся.
- Хеш-множество / словарь (Hash set / map)
Хеш находит дополнение в неотсортированных данных ценой O(n) памяти. Указателям нужен порядок, но не память.
- Два указателя (в одну сторону) (Two pointers (same direction))
Там оба индекса идут вперёд: один читает, другой пишет. Здесь они сближаются с разных концов.
- Бинарный поиск (массив) (Binary search (array))
Бинарный поиск ищет одну фиксированную цель и делит отрезок пополам. Здесь сравниваемая пара меняется каждый шаг.
n до 1e6 на отсортированных данных даёт O(n). Каждый указатель делает не больше n шагов, память O(1).
На LeetCode
Задачи на отсортированный массив или строку, которую читают с двух концов, когда нужна пара с заданной суммой, проверка палиндрома или лучшая пара. Два указателя, идущие навстречу, заменяют перебор всех пар.
Это задача урока, только позиции в ответе считаются с 1.
Сравнивай два конца, пропускай всё, что не буква и не цифра, и шагай внутрь.
Самый большой квадрат стоит на одном из концов, поэтому заполняй ответ с конца.
Сдвигай внутрь более низкую стенку, ведь она ограничивает площадь любой пары, в которой участвует.
Отсортируй числа по значению, зафиксируй одно и найди недостающую сумму двумя указателями на остатке.
Твоя задача (Tier 1)
Напиши функцию twoSumSorted(nums, target).
Найди два числа, которые в сумме дают target. Список отсортирован по возрастанию, поэтому можно идти с двух концов.
- Вход:
nums, список чисел по возрастанию, иtarget, число. Пример:[2, 7, 11, 15],9. - Возвращает: две позиции
[lo, hi]чисел, которые в сумме даютtarget. Если такой пары нет, верниnull(в Python этоNone).
Шаги:
- Поставь маркер
loв начало (позиция 0), а маркерhiна последнюю позицию. - Пока
lo < hi, считайnums[lo] + nums[hi]. Если сумма равнаtarget, верни[lo, hi]. Если сумма меньше, сдвиньloна шаг вправо. Если больше, сдвиньhiна шаг влево. - Если маркеры встретились, а пара не нашлась, верни
null.
Готово, когда проходят все тесты:
[1, 3, 4, 6, 8], target10->[2, 3][2, 7, 11, 15], target9->[0, 1][1, 2, 3, 4, 4], target8->[3, 4](есть повторы)[5], target5->null(одно число, пары нет)[], target0->null(пустой список, не должно падать)[1, 2, 3], target100->null(ни одна пара не дотягивает)
Практика
twoSumSorted
twoSumSorted(nums, target) -> [i, j] (0-based indices, i < j) or null
export function twoSumSorted(nums, target) {
// Your code here. The task is described in the box above the editor.
}
Ты здесь как гость
Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.
Лестница подсказок
Застрял? Открывай по одной, но сначала попробуй сам.