Рассмотрите следующий фрагмент кода на Python и объясните, в чём логическая ошибка алгоритма бинарного поиска, какую временную сложность он имеет и как его исправить для корректной работы на массиве с повторяющимися элементами: def binsearch(a, x): lo, hi = 0, len(a) while lo < hi: mid = (lo + hi) // 2 if a[mid] == x: return mid if a[mid]
Кратко — ошибка в обновлении границ: при использовании полуоткрытого интервала [lo,hi)[lo,hi)[lo,hi) нельзя писать lo=midlo = midlo=mid или hi=midhi = midhi=mid, потому что при hi−lo=1hi-lo=1hi−lo=1 будет mid=lo\text{mid}=lomid=lo и присваивание lo=midlo=midlo=mid не сдвинет границу (зациклит). Временная сложность алгоритма при корректной реализации — O(logn)O(\log n)O(logn). Подробно и исправления: 1) Причина ошибки - В цикле вычисляется mid=(lo+hi)//2\text{mid} = (lo+hi)//2mid=(lo+hi)//2. Если hi−lo=1hi-lo=1hi−lo=1, то mid=lo\text{mid}=lomid=lo. При ветке a[mid]<xa[\text{mid}]<xa[mid]<x код делает lo=midlo=\text{mid}lo=mid, границы не меняются и цикл бесконечен. Пример: hi−lo=1hi-lo=1hi−lo=1 (разрыв один элемент) — нет прогресса. 2) Правильный простой фикс (вернёт любой индекс с совпадением) - Для полуоткрытого интервала [lo,hi)[lo,hi)[lo,hi) обновления должны быть: - если a[mid]<xa[\text{mid}]<xa[mid]<x: lo=mid+1lo=\text{mid}+1lo=mid+1
- если a[mid]>xa[\text{mid}]>xa[mid]>x: hi=midhi=\text{mid}hi=mid
- если равно — можно вернуть mid\text{mid}mid
- Тогда гарантированно происходит сужение интервала и сложность O(logn)O(\log n)O(logn). Пример исправленного кода (любая найденная позиция): def binsearch(a, x): lo, hi = 0, len(a) while lo < hi: mid = (lo + hi) // 2 if a[mid] == x: return mid if a[mid] < x: lo = mid + 1 else: hi = mid return -1 3) Если нужен левый (первый) индекс при повторяющихся элементах - Использовать поиск границы: сужаем так, чтобы в конце lololo был индекс первой позиции ≥x\ge x≥x. После цикла проверяем равенство. Код для левого вхождения: def bisect_left(a, x): lo, hi = 0, len(a) while lo < hi: mid = (lo + hi) // 2 if a[mid] < x: lo = mid + 1 else: hi = mid return lo if lo < len(a) and a[lo] == x else -1 Аналогично для правого вхождения можно настраивать условия.
Подробно и исправления:
1) Причина ошибки
- В цикле вычисляется mid=(lo+hi)//2\text{mid} = (lo+hi)//2mid=(lo+hi)//2. Если hi−lo=1hi-lo=1hi−lo=1, то mid=lo\text{mid}=lomid=lo. При ветке a[mid]<xa[\text{mid}]<xa[mid]<x код делает lo=midlo=\text{mid}lo=mid, границы не меняются и цикл бесконечен. Пример: hi−lo=1hi-lo=1hi−lo=1 (разрыв один элемент) — нет прогресса.
2) Правильный простой фикс (вернёт любой индекс с совпадением)
- Для полуоткрытого интервала [lo,hi)[lo,hi)[lo,hi) обновления должны быть:
- если a[mid]<xa[\text{mid}]<xa[mid]<x: lo=mid+1lo=\text{mid}+1lo=mid+1 - если a[mid]>xa[\text{mid}]>xa[mid]>x: hi=midhi=\text{mid}hi=mid - если равно — можно вернуть mid\text{mid}mid - Тогда гарантированно происходит сужение интервала и сложность O(logn)O(\log n)O(logn).
Пример исправленного кода (любая найденная позиция):
def binsearch(a, x):
lo, hi = 0, len(a)
while lo < hi:
mid = (lo + hi) // 2
if a[mid] == x:
return mid
if a[mid] < x:
lo = mid + 1
else:
hi = mid
return -1
3) Если нужен левый (первый) индекс при повторяющихся элементах
- Использовать поиск границы: сужаем так, чтобы в конце lololo был индекс первой позиции ≥x\ge x≥x. После цикла проверяем равенство.
Код для левого вхождения:
def bisect_left(a, x):
lo, hi = 0, len(a)
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < x:
lo = mid + 1
else:
hi = mid
return lo if lo < len(a) and a[lo] == x else -1
Аналогично для правого вхождения можно настраивать условия.