Бинарный поиск

⏱ 40 мин · 🧩 заданий: 5

Идея

Ищем слово в бумажном словаре: открываем середину, смотрим, левее или правее нужное слово, и отбрасываем половину. Каждый шаг уменьшает область поиска вдвое → O(log n): миллион элементов — 20 шагов, миллиард — 30.

Условие: данные отсортированы (или, шире, есть монотонное условие «нет-нет-нет-да-да-да»).

def binary_search(nums, target):
    lo, hi = 0, len(nums) - 1               # ищем в отрезке [lo, hi] включительно
    while lo <= hi:
        mid = (lo + hi) // 2
        print(f"  lo={lo}, hi={hi}, mid={mid}, nums[mid]={nums[mid]}")
        if nums[mid] == target:
            return mid
        if nums[mid] < target:
            lo = mid + 1                    # target правее
        else:
            hi = mid - 1                    # target левее
    return -1

nums = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(nums, 23))
print(binary_search(nums, 7))

Пройдите пример 👣 Пошагово и посмотрите, как сужаются границы.

Инварианты — как не ошибиться

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

Вариант Отрезок Условие цикла Сдвиг
поиск точного значения [lo, hi], hi = n - 1 lo <= hi lo = mid + 1, hi = mid - 1
первый «да» (lower bound) [lo, hi), hi = n lo < hi «нет» → lo = mid + 1, «да» → hi = mid

Второй вариант универсален: ищем первую позицию, где условие становится истинным.

def first_true(lo, hi, cond):
    """Первое x в [lo, hi), для которого cond(x) истинно (cond монотонно: F F F T T T)."""
    while lo < hi:
        mid = (lo + hi) // 2
        if cond(mid):
            hi = mid          # mid подходит — ответ mid или левее
        else:
            lo = mid + 1      # mid не подходит — ответ правее
    return lo

nums = [1, 2, 2, 2, 5, 7]
first = first_true(0, len(nums), lambda i: nums[i] >= 2)      # первое вхождение 2
after = first_true(0, len(nums), lambda i: nums[i] > 2)       # первое больше 2
print(first, after, "→ двойка встречается", after - first, "раза")

В Python нет переполнения

В C/Java (lo + hi) / 2 может переполниться, поэтому пишут lo + (hi - lo) / 2. В Python целые числа неограниченные — (lo + hi) // 2 безопасно, но о переполнении могут спросить.

Модуль bisect

import bisect

scores = [10, 20, 20, 20, 30, 40]
print(bisect.bisect_left(scores, 20))       # первая позиция, куда можно вставить 20 — слева от равных
print(bisect.bisect_right(scores, 20))      # справа от равных
print(bisect.bisect_left(scores, 25))

bisect.insort(scores, 25)                   # вставка с сохранением порядка (поиск O(log n), вставка O(n))
print(scores)

def grade(score, breakpoints=(60, 70, 80, 90), grades="FDCBA"):
    return grades[bisect.bisect(breakpoints, score)]
print([grade(s) for s in (33, 60, 77, 89, 90, 100)])

events = [(1, "старт"), (5, "обед"), (9, "отчёт")]      # поиск по ключу (Python 3.10+)
i = bisect.bisect_right(events, 6, key=lambda e: e[0])
print("последнее событие до 6:", events[i - 1])

На собеседовании обычно просят написать поиск самому, но знать bisect полезно.

Поиск по ответу

Мощный приём: если ответ — число, и можно быстро проверить, подходит ли кандидат, причём проверка монотонна («если подходит k, подходит и любое большее»), — ищем минимальный подходящий k бинарным поиском по значениям.

def min_capacity(weights, days):
    """Минимальная грузоподъёмность корабля, чтобы перевезти посылки по порядку за days дней."""
    def can_ship(capacity):
        need_days, load = 1, 0
        for w in weights:
            if load + w > capacity:
                need_days += 1
                load = 0
            load += w
        return need_days <= days

    lo, hi = max(weights), sum(weights)      # ответ точно в этом диапазоне
    while lo < hi:
        mid = (lo + hi) // 2
        if can_ship(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

print(min_capacity([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5))

def isqrt(n):
    """Целая часть квадратного корня без math."""
    lo, hi = 0, n + 1
    while lo < hi:                       # первое x, у которого x*x > n
        mid = (lo + hi) // 2
        if mid * mid > n:
            hi = mid
        else:
            lo = mid + 1
    return lo - 1

print(isqrt(17), isqrt(10**20), isqrt(0))

Признаки: «минимальная скорость/вместимость/время, при которой…», «максимальное минимальное расстояние», «разбить на k частей с минимальной максимальной суммой».

Повёрнутый массив и пик

def search_rotated(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:                  # левая половина отсортирована
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        else:                                       # правая половина отсортирована
            if nums[mid] < target <= nums[hi]:
                lo = mid + 1
            else:
                hi = mid - 1
    return -1

print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0), search_rotated([4, 5, 6, 7, 0, 1, 2], 3))

def find_peak(nums):
    """Индекс любого элемента, который больше соседей."""
    lo, hi = 0, len(nums) - 1
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] < nums[mid + 1]:
            lo = mid + 1                            # подъём — пик правее
        else:
            hi = mid                                # спуск — пик здесь или левее
    return lo

print(find_peak([1, 2, 1, 3, 5, 6, 4]))

Итог

  • Бинарный поиск — O(log n) на монотонных данных; каждый шаг отбрасывает половину.
  • Держите инвариант: [lo, hi] + lo <= hi для точного поиска, [lo, hi) + lo < hi для «первого подходящего».
  • bisect_left/bisect_right/insort — готовые функции.
  • Поиск по ответу: бинарный поиск по значению с функцией проверки.
  • Повёрнутый массив и пик — бинарный поиск по признаку половины.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Классический бинарный поиск. Верните индекс target в отсортированном списке nums или -1. Не используйте bisect и index — напишите цикл с двумя границами.

Какие тесты проверяют код (3)
assert binary_search([-1, 0, 3, 5, 9, 12], 9) == 4 and binary_search([-1, 0, 3, 5, 9, 12], 2) == -1
assert binary_search([], 1) == -1 and binary_search([5], 5) == 0 and binary_search([1, 2], 2) == 1
class Spy(list):
    reads = 0
    def __getitem__(self, i):
        Spy.reads += 1
        return super().__getitem__(i)
nums = Spy(range(0, 200_000, 2))
assert binary_search(nums, 199_998) == 99_999 and binary_search(nums, 7) == -1
assert 0 < Spy.reads < 100, f"Прочитано элементов по индексу: {Spy.reads} — нужен O(log n) через nums[mid]"

Задание 2 Программа на Python

Первое вхождение (lower bound). Верните индекс первого элемента, который >= target (если все меньше — len(nums)). Это аналог bisect.bisect_left, но напишите его сами. Через неё легко найти, сколько раз число встречается в отсортированном списке.

Какие тесты проверяют код (3)
assert lower_bound([1, 2, 2, 2, 5], 2) == 1 and lower_bound([1, 2, 2, 2, 5], 3) == 4
assert lower_bound([1, 2, 3], 0) == 0 and lower_bound([1, 2, 3], 9) == 3 and lower_bound([], 5) == 0
import bisect, random; r = random.Random(7); data = sorted(r.randint(0, 50) for _ in range(200)); assert all(lower_bound(data, t) == bisect.bisect_left(data, t) for t in range(-2, 55))
Задание 3 Программа на Python

Поиск по ответу: Коко и бананы. Есть кучи бананов piles и h часов. За час Коко съедает до k бананов из одной кучи (если в куче меньше — доедает её и ждёт следующего часа). Найдите минимальное целое k, при котором она успеет за h часов (h >= len(piles)). Куча может быть до 10⁹ бананов — перебор k по одному не подойдёт.

Какие тесты проверяют код (3)
assert min_eating_speed([3, 6, 7, 11], 8) == 4 and min_eating_speed([30, 11, 23, 4, 20], 5) == 30
assert min_eating_speed([30, 11, 23, 4, 20], 6) == 23 and min_eating_speed([1], 1) == 1
import time
start = time.perf_counter()
assert min_eating_speed([10**9] * 3, 3) == 10**9
assert time.perf_counter() - start < 0.2, "Перебор k по одному слишком медленный — ищите k бинарным поиском"

Задание 4 Программа на Python

Минимум в повёрнутом массиве. Отсортированный массив без повторов «повернули»: [0, 1, 2, 4, 5, 6, 7] → [4, 5, 6, 7, 0, 1, 2]. Найдите минимальный элемент за O(log n).

Какие тесты проверяют код (3)
assert find_min_rotated([4, 5, 6, 7, 0, 1, 2]) == 0 and find_min_rotated([3, 4, 5, 1, 2]) == 1
assert find_min_rotated([11, 13, 15, 17]) == 11 and find_min_rotated([2, 1]) == 1 and find_min_rotated([1]) == 1
class Spy(list):
    reads = 0
    def __getitem__(self, i):
        Spy.reads += 1
        return super().__getitem__(i)
data = Spy(list(range(500_000, 1_000_000)) + list(range(500_000)))
assert find_min_rotated(data) == 0
assert 0 < Spy.reads < 200, f"Прочитано по индексу {Spy.reads} элементов — нужно O(log n) через nums[mid]"

Задание 5 Один вариант ответа

Чем опасна запись while lo < hi: mid = (lo + hi) // 2; if cond: lo = mid else: hi = mid - 1?

Войдите, чтобы отмечать пройденные уроки.