Условия задачи
Нам нужно написать функцию, которая принимает отсортированный массив чисел numberArray
и возвращает индекс найденного числа. Если индекс не найден, тогда возвращается -1
.
Сразу уделю внимание на то, что длинна массива может быть любой. Массив может состоять из любых чисел и искомое число так же может быть любым.
Предположим у нас есть массив чисел от 1 до 100:
const numberArray [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,
97, 98, 99, 100
]
Линейный поиск
Начнем с определения.
Линейный поиск — это простой алгоритм поиска элемента в структуре данных (например в массиве), который последовательно проверяет каждый элемент в структуре данных на совпадение с целевым значением. Он начинает с первого элемента и продолжает проверку до тех пор, пока не будет найден искомый элемент или пока не закончится весь набор данных.
Напишем функцию, которая будет проверять все элементы массива по очереди:
const linearSearch = (numbersArray, targetNumber) => {
for (let i = 0; i < numbersArray.length; i++) {
if (numbersArray[i] === targetNumber) {
return i // Элемент найден
}
}
return -1; // Элемент не найден
}
Если в качестве targetNumber
мы передадим 1
, тогда нам потребуется одна итерация цикла, чтобы найти число.
Если в качестве targetNumber
мы передадим 100
, тогда нам потребуется 100 итераций цикла, чтобы найти число.
В данном алгоритме сложность O(n). Такую сложность так же называют линейной сложностью.
Такой поиск крайне не эффективный при работе с большим набором данных.
Бинарный поиск
Бинарный поиск гораздо более эффективный в сравнении с линейным поиском.
Бинарный поиск основан на идее деления данных на половины и последующем поиске в одной из них с последующим делением.
Принцип бинарного поиска
Предположим, что в нашем отсортированном списке чисел от 1 до 100 мы будем искать число 87.
Первое действие:
Разделим наш массив пополам. У нас получилось 2 массива
one
иtwo
. Вone
числа от 1 до 50, а вtwo
с 51 до 100.
const one = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 50
]
const two = [
51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100
]
Посмотрим последний элемент массива
one
. 49 больше, чем 87? Нет.Тогда смотрим последний элемент массива
two
. 100 больше, чем 87? Да, значит искать число будет массивеtwo
. При первом действии мы уже отсекли половину данных.
Второе действие:
-
Массив
two
снова раздели на два. Получаем массивthree
c числами от 50 до 75 иfour
с числами от 76 до 100.const three = [ 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75 ] const four = [ 76, 77, 78, 79, 80, 81, 82, 83, 84, 95, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 96, 97, 98, 99, 100 ]
Посмотрим последний элемент массива
three
. 75 больше, чем 87? Нет.Тогда смотрим последний элемент массива
four
. 100 больше, чем 87? Да, значит искать число будет в массивеfour
. При втором действии мы отсекли еще половину данных.
Третье действие:
-
Массив
four
снова раздели на два. Получаем массивfive
с числами от 76 до 87 и массивsix
с числами от 88 до 100.const five = [ 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, ] const six = [ 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 ]
Посмотрим последний элемент массива
five
.87 больше, чем 87? Нет. Они равны. Мы нашли наше число на третьем действии.
Алгоритм бинарного поиска
Бинарный поиск работает только в том случае, если список отсортирован!
По условию задачи у нас может быть любая длинна массива и любые отсортированные числа в начале массива.
Напишем решение для нашей задачи с использованием бинарного поиска.
Для начала нам нужно генерировать подобные массивы.
Напишем функцию getRandomNumber
, которая возвращает рандомное число в заданном диапазоне:
const getRandomNumber = (min, max) => {
// Метод Math.ceil() - округление вверх. Округляет аргумент до ближайшего большего целого.
min = Math.ceil(min);
// Метод Math.floor() - округление вниз. Округляет аргумент до ближайшего меньшего целого.
max = Math.floor(max);
// Максимум не включается, минимум включается
return Math.floor(Math.random() * (max - min) + min);
}
Напишем функцию createRandomNumberArray
, которая возвращает массив чисел заданной длинны с отсортированными числами.
const createRandomNumberArray = (arrayLength, firstNumber) => {
return Array.from({ length: arrayLength }, (_, index) => index + firstNumber);
}
Далее напишем функцию binarySearch
, которая будет возвращать объект с найденным индексом (или -1) и с количеством итераций цикла, которые мы прошли.
const binarySearch = (numbersArray, targetNumber) => {
let left = 0;
let right = numbersArray.length - 1;
let foundIndex = -1
let operations = 0
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (numbersArray[mid] === targetNumber) {
foundIndex = mid; // Элемент найден
break;
} else if (numbersArray[mid] < targetNumber) {
left = mid + 1; // Ищем в правой половине
} else {
right = mid - 1; // Ищем в левой половине
}
operations += 1
}
return { foundIndex, operations }
}
Так же напишем функцию linearSearch
c обычным линейным поиском, которая будет возвращать объект с найденным индексом или -1 и с количеством итераций цикла, которые мы прошли.
const linearSearch = (numbersArray, targetNumber) => {
let foundIndex = -1
let operations = 0
for (let i = 0; i < numbersArray.length; i++) {
if (numbersArray[i] === targetNumber) {
foundIndex = i
return { foundIndex, operations }; // Элемент найден
}
operations += 1
}
return { foundIndex, operations }; // Элемент не найден
}
Получим следующие данные:
// Случайная длина массива
const arrayLength = getRandomNumber(1000, 2000)
// Случайное число с которого будут начинаться наши числа.
const firstNumber = getRandomNumber(1, 1000)
// Случайное число, которое мы будем искать
const targetNumber = getRandomNumber(firstNumber, 1000)
И наконец-то создадим сам массив:
// Массив чисел
const numbersArray = createRandomNumberArray(arrayLength, firstNumber)
И в конце будем запускать наш код и сравним количество итераций цикла в функции и функции:
const binaryResult = binarySearch(numbersArray, targetNumber)
const linearResult = linearSearch(numbersArray, targetNumber)
console.log('arrayLength', arrayLength)
console.log('firstNumber', firstNumber)
console.log('targetNumber', targetNumber)
console.log('binaryResult', binaryResult)
console.log('linearResult', linearResult)
Полная версия кода:
const getRandomNumber = (min, max) => {
// Метод Math.ceil() - округление вверх. Округляет аргумент до ближайшего большего целого.
min = Math.ceil(min);
// Метод Math.floor() - округление вниз. Округляет аргумент до ближайшего меньшего целого.
max = Math.floor(max);
// Максимум не включается, минимум включается
return Math.floor(Math.random() * (max - min) + min);
}
const createRandomNumberArray = (arrayLength, firstNumber) => {
return Array.from({ length: arrayLength }, (_, index) => index + firstNumber);
}
const binarySearch = (numbersArray, targetNumber) => {
let left = 0;
let right = numbersArray.length - 1;
let foundIndex = -1
let operations = 0
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (numbersArray[mid] === targetNumber) {
foundIndex = mid; // Элемент найден
break;
} else if (numbersArray[mid] < targetNumber) {
left = mid + 1; // Ищем в правой половине
} else {
right = mid - 1; // Ищем в левой половине
}
operations += 1
}
return { foundIndex, operations }
}
const linearSearch = (numbersArray, targetNumber) => {
let foundIndex = -1
let operations = 0
for (let i = 0; i < numbersArray.length; i++) {
if (numbersArray[i] === targetNumber) {
foundIndex = i
return { foundIndex, operations }; // Элемент найден
}
operations += 1
}
return { foundIndex, operations }; // Элемент не найден
}
// Случайная длина массива
const arrayLength = getRandomNumber(1000, 2000)
// Случайное число с которого будут начинаться наши числа.
const firstNumber = getRandomNumber(1, 1000)
// Случайное число, которое мы будем искать
const targetNumber = getRandomNumber(firstNumber, 1000)
// Массив чисел
const numbersArray = createRandomNumberArray(arrayLength, firstNumber)
const binarySearch = (numbersArray, targetNumber) => {
let left = 0;
let right = numbersArray.length - 1;
let foundIndex = -1
let operations = 0
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (numbersArray[mid] === targetNumber) {
foundIndex = mid; // Элемент найден
break;
} else if (numbersArray[mid] < targetNumber) {
left = mid + 1; // Ищем в правой половине
} else {
right = mid - 1; // Ищем в левой половине
}
operations += 1
}
return { foundIndex, operations }
}
const binaryResult = binarySearch(numbersArray, targetNumber)
const linearResult = linearSearch(numbersArray, targetNumber)
console.log('arrayLength', arrayLength)
console.log('firstNumber', firstNumber)
console.log('targetNumber', targetNumber)
console.log('binaryResult', binaryResult)
console.log('linearResult', linearResult)
Смотрим консоль с результатами выполнения:
arrayLength 1376
firstNumber 499
targetNumber 995
binaryResult { foundIndex: 496, operations: 9 }
linearResult { foundIndex: 496, operations: 496 }
arrayLength 1158
firstNumber 785
targetNumber 817
binaryResult { foundIndex: 32, operations: 8 }
linearResult { foundIndex: 32, operations: 32 }
arrayLength 1002
firstNumber 671
targetNumber 822
binaryResult { foundIndex: 151, operations: 7 }
linearResult { foundIndex: 151, operations: 151 }
arrayLength 1235
firstNumber 951
targetNumber 970
binaryResult { foundIndex: 19, operations: 9 }
linearResult { foundIndex: 19, operations: 19 }
arrayLength 1599
firstNumber 649
targetNumber 940
binaryResult { foundIndex: 291, operations: 10 }
linearResult { foundIndex: 291, operations: 291 }
В этих пяти случаях:
Для бинарного поиска потребовалось от 7 до 10 итераций цикла.
Для линейного поиска потребовалось от 19 до 496 итераций цикла.
Для линейного поиска максимальное количество итераций совпадает с размером массива.
С бинарным поиском дело обстоит иначе. Если список состоит из 100 элементов, потребуется не более 7 попыток. Для списка из 4 миллиардов элементов потребуется не более 32 попыток. Впечатляет, верно?
Бинарный поиск выполняется за логарифмическое время и имеет сложность O(log n).
Логарифмы
Логарифм - это математическая функция, которая говорит нам, сколько раз нужно умножить определенное число (называемое основанием логарифма) на само себя, чтобы получить другое число.
Давай представим, что у нас есть число 1000. Если мы возьмем логарифм числа 1000 по основанию 10, результат будет 3. Это означает, что 10 умноженное на себя три раза (10 * 10 * 10) равно 1000.
Логарифм − операция, обратная возведению в степень.
В нашем случае log всегда означает log по основанию 2.
Для бинарного поиска в худшем случае потребуется не более log n проверок.
Для списка из 8 элементов log 8 потребуется не более 3 проверок, потому что 2^3 = 8.
Для списка из 1024 элементов log 1024 не более 10 проверок, потому что 2^10 = 1024.
Комментарии (11)
Deosis
28.12.2023 15:22+1const mid = Math.floor((left + right) / 2);
Знаменитый баг, который ломает бинарный поиск на больших массивах.
mayorovp
28.12.2023 15:22Конкретно Math.floor ничего не ломает, оно просто медленное.
Чтобы его сломать, нужны индексы более . Я не верю в такие массивы на Javascript.
Neoldian
28.12.2023 15:22-1Что забавно, почти в каждой статье или заметке по бинарному поиску пишут именно так) Что бы не ломать потом голову лучше писать a + (b-a)/2 , и не будет никакого переполнения.
mayorovp
28.12.2023 15:22Если написать a + (b-a)/2 - может получиться нецелый индекс. Числа-то в Javascript вещественные...
Neoldian
28.12.2023 15:22Приведение к целому конечно необходимо, я про то что именно выражение лучше писать a + (b-a)/2, в js оно как раз без последствий. Статья то образовательная, и не хочется чтобы у кого то, кто перепишет на более близких к железу языках как (a+b)/2, была головная боль с overflow)
Metotron0
28.12.2023 15:22Мне помнится, что в книге «Грокаем алгоритмы» бинарный поиск расписан очень подробно. Неужели была необходимость в ещё одном объяснении, а не в ссылке на страницу книги?
Собственно, поэтому я и не пишу статьи на хабр: они будут состоять только из ссылки.
qss53770
28.12.2023 15:22+1Да про бин поиск везде написано куча читай не хочу, автор недавно видимо сделал это открытие и думает дай ка напишу на хбре про стандартную задачу и стандартный алгоритм
nameisBegemot
Найти число бинарным поиском это конечно замечательно. Но в реальности чаще возникает другая задача - как найти период некоторых чисел, которые старше некоторого порогового значения. Например, из массива в 100 чисел найти индексы ячеек от 75.
Прямо в лоб поделить массив пополам мало. Нужно найти границы интервалов. Код несложный, но интереснее поиска отдельного числа.
А да, поиск интервалов все тот же бинарный поиск.