Сигнал задачи
Когда применять
Есть монотонный предикат или упорядоченная граница.
От перебора к короткому примеру
Линейный поиск проверяет до n элементов. В отсортированном массиве сравнение середины с target позволяет сразу отбросить часть значений. Покажем lower bound: первый индекс, где a[i] ≥ target; если такого элемента нет, результат n.
Инвариант
Все индексы < left содержат значения < target; все индексы ≥ right содержат значения ≥ target. Не классифицированы элементы [left, right), а искомая позиция остаётся в [left, right], включая n. При left = right граница найдена.
- a = [1, 3, 3, 8], target = 3. left = 0, right = 4; mid = 2, a[mid] = 3: сохраняем середину кандидатом, right = 2.
- mid = 1, a[mid] = 3: right = 1. Затем mid = 0, a[mid] = 1: left = 1.
- left = right = 1: получили первое вхождение 3. Для точного поиска нужно проверить i < n и a[i] == target.
Код на C++ и Python
Обе версии реализуют один контракт. Выберите язык кнопками над кодом; примеры проверяются на маленьких входах и граничных случаях.
C++17: первый индекс с a[i] ≥ target
#include <vector>
int first_not_less(const std::vector<int>& a, int target) {
int left = 0;
int right = static_cast<int>(a.size());
while (left < right) {
int mid = left + (right - left) / 2;
if (a[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}Python 3: первый индекс с a[i] ≥ target
def first_not_less(a, target):
left, right = 0, len(a)
while left < right:
mid = left + (right - left) // 2
if a[mid] < target:
left = mid + 1
else:
right = mid
return leftПроверьте контракт
- [] и target = 3 → 0
- [1, 3, 3, 8] и target = 3 → 1
- [1, 3, 3, 8] и target = 4 → 3
- [1, 3, 3, 8] и target = 9 → 4
Сложность и ограничения метода
- Пустой массив возвращает 0; если target больше всех элементов, возвращается n. Не читать a[n]. Повторы не мешают lower bound находить первый подходящий индекс.
- Каждый шаг сокращает неклассифицированный диапазон примерно вдвое: O(log n) времени при O(1) доступе к середине и сравнении; дополнительная память O(1). Сортировка исходного массива — отдельная работа, обычно O(n log n). Для одного запроса линейный поиск может быть выгоднее.
- C++: предполагаем, что размер массива помещается в int. Python: list поддерживает доступ по индексу; стоимость операций над большими целыми зависит от разрядности.
- Готовые функции: std::lower_bound в C++ и bisect_left в Python. Вставка в середину массива после поиска всё равно требует O(n) перемещений. Бинарное дерево поиска — другая структура данных.
Следующий шаг
Что держать в голове
- Уметь поддерживать инвариант закрытого диапазона.
- Уметь избегать переполнения midpoint и зависания границ.
Сложность
- Поиск по индексируемому упорядоченному пространству: O(log n) времени и O(1) памяти в итеративной форме.
Границы и ошибки
- Контракт границ и монотонность предиката важнее формулы mid; смешение закрытого диапазона и полуинтервала вызывает зависание или off-by-one.