В этом уроке ты
- Поймешь, что такое бинарный поиск и в чем его преимущество.
- Решишь задачу с собеседования методом бинарного поиска.
- На практике оценишь время и память, которые тратит решение, используя Big O нотацию.
Зачем нужен бинарный поиск
Представь, что у тебя есть уже отсортированный массив, например: , в котором 128 элементов. И тебе нужно проверить, есть ли в этом массиве число nums = [1, 3, 4, 5, 8, 15, 15, …].x
Линейный поиск
Самое простое решение — линейный поиск, который просто идет по всем элементам слева направо и сравнивает каждый с :x
export function search(nums, x) {
for (const num of nums) {
if (num === x) {
return true;
}
}
return false;
}
export function search(nums, x) {
for (const num of nums) {
if (num === x) {
return true;
}
}
return false;
}
export function search(nums, x) {
for (const num of nums) {
if (num === x) {
return true;
}
}
return false;
}
export function search(nums, x) {
for (const num of nums) {
if (num === x) {
return true;
}
}
return false;
}
export function search(nums, x) {
for (const num of nums) {
if (num === x) {
return true;
}
}
return false;
}
export function search(nums, x) {
for (const num of nums) {
if (num === x) {
return true;
}
}
return false;
}
Даже если учесть, что массив отсортирован, и остановить цикл, когда текущее число стало больше x:
class Solution
{
static bool Search(int[] nums, int x)
{
int i = 0;
while (i < nums.Length && nums[i] <= x)
{
if (nums[i] == x)
{
return true;
}
i++;
}
return false;
}
}
#include <iostream>
#include <vector>
using namespace std;
bool search(const vector<int>& nums, int x) {
int i = 0;
while (i < nums.size() && nums[i] <= x) {
if (nums[i] == x) {
return true;
}
i++;
}
return false;
}
package main
import "fmt"
func search(nums []int, x int) bool {
i := 0
for i < len(nums) && nums[i] <= x {
if nums[i] == x {
return true
}
i++
}
return false
}
public class Solution {
public static boolean search(int[] nums, int x) {
int i = 0;
while (i < nums.length && nums[i] <= x) {
if (nums[i] == x) {
return true;
}
i++;
}
return false;
}
}
def search(nums: List[int], x) -> bool:
i = 0
while i < len(nums) and nums[i] <= x:
if nums[i] == x:
return True
i += 1
return False
export function search(nums, x) {
let i = 0;
while (i < nums.length && nums[i] <= x) {
if (nums[i] === x) {
return true;
}
i++;
}
return false;
}
Все равно в худшем случае может понадобиться посмотреть почти на каждый элемент (если x окажется в конце или его вовсе не окажется). Такой поиск работает за O(n), то есть время поиска растет пропорционально количеству элементов.
Бинарный поиск
Бинарный поиск позволяет работать гораздо быстрее — за . Бинарный поиск элемента в массиве размером O(log(n)) займет всего 1024 шагов вместо 10 при линейном поиске. А при размере 1024 - всего 1 048 576.20
Что такое бинарный поиск
Бинарный поиск — это алгоритм, который эффективно находит нужный элемент в отсортированном массиве.
Во многих языках программирования есть готовые методы, которые делают это за нас. Но на собеседованиях часто просят написать бинарный поиск самостоятельно.
Почему бинарный поиск быстрый
Допустим, у нас есть массив , и мы хотим узнать, есть ли в нем число [2, 4, 4, 8, 14, 15, 21].15
Шаг 1: заводим два указателя.
L(левый) — в начале стоит на индексе0R(правый) — обычно ставим на индексlen(nums), то есть один элемент «за концом» массива.
Это немного непривычно, ведь мы указываем за границу массива, но так алгоритм получится проще и избежит лишних проверок.
Шаг 2: находим середину.
- Вычислим
M = (L + R) // 2, то есть средний индекс. - Сравним
nums[M]с искомым числом (15в нашем примере).
Шаг 3: двигаем указатели.
- Если
nums[M] <= 15, значит наша цель точно не «левее» индексаM(ибо там элементы еще меньше). Поэтому мы «подвигаем» левый указательLна серединуM. - Если
nums[M] > 15, то, наоборот, двигаем правый указательRнаM, так как все элементы правееMмогут оказаться слишком большими.
Шаг 4, 5, …: Повторяем эти шаги, пока , т.е. пока R - L > 1 и R не окажутся рядом.L
Получаем ответ: Когда и L сойдутся вплотную, мы проверим элемент по индексу R, чтобы понять, равен ли он L.15
В результате у нас постоянно уменьшается диапазон, где может быть искомое число. Вместо того, чтобы просматривать все элементы подряд, мы каждую итерацию «отсекаем» половину вариантов, и поэтому работаем значительно быстрее.
Код
class Solution
{
static bool Search(int[] nums, int target)
{
int l = 0, r = nums.Length;
while (r - l > 1)
{
int m = (l + r) / 2;
if (nums[m] <= target)
{
l = m;
}
else
{
r = m;
}
}
return nums[l] == target;
}
}
#include <iostream>
#include <vector>
using namespace std;
bool search(const vector<int>& nums, int target) {
int l = 0, r = nums.size();
while (r - l > 1) {
int m = (l + r) / 2;
if (nums[m] <= target) {
l = m;
} else {
r = m;
}
}
return nums[l] == target;
}
package main
import "fmt"
func search(nums []int, target int) bool {
l, r := 0, len(nums)
for r-l > 1 {
m := (l + r) / 2
if nums[m] <= target {
l = m
} else {
r = m
}
}
return nums[l] == target
}
public class Solution {
public static boolean search(int[] nums, int target) {
int l = 0, r = nums.length;
while (r - l > 1) {
int m = (l + r) / 2;
if (nums[m] <= target) {
l = m;
} else {
r = m;
}
}
return nums[l] == target;
}
}
from typing import *
def search(nums: List[int], target: int) -> int:
# Начало бинарного поиска
l, r = 0, len(nums) # Изменение 1: могут меняться начальные значения
while r - l > 1:
m = (l + r) // 2
if nums[m] <= target: # Изменение 2: может меняться условие
l = m
else:
r = m
# Конец бинарного поиска
return bool(nums[l] == target)
export function search(nums, target) {
let l = 0, r = nums.length;
while (r - l > 1) {
const m = Math.floor((l + r) / 2);
if (nums[m] <= target) {
l = m;
} else {
r = m;
}
}
return nums[l] === target;
}
Когда цикл завершается, указывает на позицию, где находится последний элемент, не превышающий l. Если target совпадает с nums[l], мы возвращаем target. Если нет — true.false
Почему R = len(nums)
Теперь к вопросу, почему мы ставим не на R, а именно на len(nums) - 1.len(nums)
- Искомый элемент может оказаться последним (к примеру,
21в массиве[2, 4, 4, 8, 14, 15, 21]). Если бы мы изначально поставилиR = len(nums) - 1, то при поиске последнего элемента бинарный поиск завершится неправильно. - Ставя
R = len(nums), мы избавляемся от лишних проверок (алгоритм сам никогда не «выйдет» за границы реального массива) и получаем аккуратное условие выхода из цикла.
Установка R = len(nums) — это удобный трюк, чтобы проще обрабатывать случаи поиска элемента в самом конце массива, без дополнительных условий.
Результат и выводы
Скорость: бинарный поиск отбрасывает половину массива на каждом шаге и за счет этого работает намного быстрее линейного поиска.
Оценка сложности: бинарный поиск в массиве из n элементов будет работать за по времени и O(log(n)) по дополнительной памяти.O(1)
Важность сортировки: когда выполняем классический бинарный поиск, то массив должен быть обязательно отсортирован, но в дальнейшем ты научишься применять метод бинарного поиска даже для данных, где нет сортировки.
Что дальше
Скорее переходи к задачам, чтобы отработать на практике бинарный поиск.