Инструменты пользователя

Инструменты сайта


basics_of_algorithms:binary_search

Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

Предыдущая версия справа и слева Предыдущая версия
Следующая версия
Предыдущая версия
basics_of_algorithms:binary_search [2023/10/03 19:07]
werwolf [Недостатки бинарного поиска]
basics_of_algorithms:binary_search [2023/10/03 19:43] (текущий)
werwolf [Как реализовать бинарный поиск]
Строка 1: Строка 1:
-==== Недостатки бинарного поиска ​====+====== Бинарный поиск — Основы алгоритмов и структур данных ====== 
 +Мы постоянно что-то ищем с помощью компьютера:​ номера телефона,​ свободные номера в гостиницах, товары в интернет-магазинах, квартиры в аренду. Даже сайты мы ищем с помощью поисковой машины. За каждым из этих поисков скрываются разные алгоритмы. Среди них — поиск ​подстроки,​ поиск по ключевым словам, префиксный поиск.
  
 +В этом уроке мы познакомимся с двумя алгоритмами — **методом перебора** и **бинарным поиском**.
 +
 +===== Метод перебора =====
 +
 +Алгоритм перебора проверяет все значения в списке с начала и до нужного атрибута,​ поэтому его также называют **последовательным** или **линейным поиском**.
 +
 +Начнем с самого простого алгоритма перебора — **поиска по списку**.
 +
 +Представим,​ что мы хотим найти в Google все страницы,​ на которых встречается фраза «А роза упала на лапу Азора». Слова «роза»,​ «упала» и «лапа» встречаются редко и только в этой фразе, поэтому они значимы для поиска.
 +
 +А вот слова «а» и «на» встречаются в огромном количестве других запросов и в самых разных предложениях. На результаты поиска они не влияют,​ потому что поисковые машины исключают их из запроса.
 +
 +Поисковик примет фразу «а роза упала на лапу Азора» и превратит ее в «роза упала лапу Азора». Таким образом он уберет из запроса **стоп-слова**. К ним относятся союзы, частицы,​ предлоги,​ междометия и некоторые другие части речи.
 +
 +У каждой поисковой машины есть свой список стоп-слов,​ которые она автоматически исключает из текста. Для примера возьмем такой список стоп-слов в случайном порядке:​
 +
 +| или | вас | для | после | ого | раньше |
 +| но | ее | на | во | эх | позже |
 +| дабы | что | по | не | браво | когда-то |
 +| затем | который | со | же | здравствуйте | очень |
 +| потом | их | из | то | спасибо | минимально |
 +| лишь | все | от | бы | извините | максимально |
 +| только | они | до | всего | что-то | а |
 +| он | я | без | итого | какой-то | огромный |
 +| мы | весь | над | даже | где-то | предельно |
 +| его | мне | под | да | как-то | сильно |
 +| вы | меня | за | нет | дальше | слабо |
 +| вам | таким | при | ой | ближе | самый |
 +
 +Сейчас таблица неудобная — в ней нет какого-либо порядка. Чтобы найти нужное слово, придется постоянно просматривать весь список,​ тратить время и силы. Работа пойдет быстрее и проще, если упорядочить слова по алфавиту. Слова упорядочены по колонкам:​
 +
 +| а | где-то | здравствуйте | меня | он | самый |
 +| без | да | из | минимально | они | сильно |
 +| ближе | дабы | извините | мне | от | слабо |
 +| браво | даже | или | мы | очень | со |
 +| бы | дальше | итого | на | по | спасибо |
 +| вам | для | их | над | под | таким |
 +| вас | до | как-то | не | позже | то |
 +| весь | его | какой-то | нет | после | только |
 +| во | ее | когда-то | но | потом | что |
 +| все | же | который | ого | предельно | что-то |
 +| всего | за | лишь | огромный | при | эх |
 +| вы | затем | максимально | ой | раньше | я |
 +
 +Рассмотрим более сложный вариант метода перебора — **поиск по ключу**. Такой алгоритм помогает узнать,​ встречается ли искомое слово в нашем списке. Для примера возьмем массив с европейскими столицами и датами их основания:​
 +
 +| 1804 | Вена | 1237 | Берлин | 1323 | Вильнюс |
 +| 1614 | Тирана | 1167 | Копенгаген | 963 | Люксембург |
 +| 1067 | Минск | 1191 | Дублин | 1275 | Амстердам |
 +| 752 | Ватикан | 1786 | Рейкьявик | 1200 | Варшава |
 +| 1872 | Будапешт | 856 | Мадрид | 1147 | Москва |
 +
 +Какой город основали в 1275 году? Чтобы ответить,​ придется перебрать все записи в списке,​ потому что они расположены в случайном порядке.
 +
 +Упорядочим города по году основания,​ и тогда ответ будет очевиден:​
 +
 +| 752 | Ватикан | 1167 | Копенгаген | 1323 | Вильнюс |
 +| 856 | Мадрид | 1191 | Дублин | 1614 | Тирана |
 +| 963 | Люксембург | 1200 | Варшава | 1786 | Рейкьявик |
 +| 1067 | Минск | 1237 | Берлин | 1804 | Вена |
 +| 1147 | Москва | 1275 | Амстердам | 1872 | Будапешт |
 +
 +Эти два примера показывают нам два варианта поиска по списку:​
 +
 +  * В примере со стоп-словами мы проверяли,​ есть ли слово в списке. Это самый простой вид поиска ​
 +  * В примере со столицами мы знали один **атрибут** города — год основания,​ и мы хотели узнать другой атрибут — название. Атрибут,​ по которому мы ищем, называется **ключом**,​ а сам метод — **поиском по ключу** ​
 +
 +По обоим примерам видно, насколько медленно работает метод перебора. В худшем случае алгоритму придется проверить практически все значения в списке.
 +
 +Представим,​ что по такому методу работает проверка логина и пароля в Facebook. Массив с логинами-паролями всех пользователей содержит около трех миллиардов элементов. Даже на самых быстрых серверах поиск в массиве занимал бы несколько минут.
 +
 +Чтобы не тратить так много времени на авторизацию в Facebook и работу с другими массивами,​ можно использовать более быстрый алгоритм.
 +
 +===== Как ускорить алгоритм перебора =====
 +
 +Мы без труда находим нужную информацию в справочниках и словарях,​ потому что в них словарные статьи упорядочены. Чтобы работать с такими массивами,​ люди пользуются неким алгоритмом. Сложность в том, что это происходит на интуитивном уровне — формально описать такой алгоритм непросто.
 +
 +В то же время компьютеру нужны конкретные инструкции,​ так что нам придется превратить интуитивное знание в алгоритм.
 +
 +Чтобы это сделать,​ начнем с простого метода перебора:​
 +
 +<code javascript>​
 +const stopWords = ['​ее',​ '​на',​ '​по',​ '​со',​ '​же',​ '​браво',​ '​всего',​ '​я',​ '​итого'​];​
 +
 +const isStopWord = (candidate) => {
 +  for (let i = 0; i < stopWords.length;​ i += 1) {
 +    if (stopWords[i] === candidate) {
 +      return true;
 +    }
 +  }
 +
 +  return false;
 +};
 +</​code>​
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +stop_words = ['​ее',​ '​на',​ '​по',​ '​со',​ '​же',​ '​браво',​ '​всего',​ '​я',​ '​итого'​]
 +
 +def is_stop_word(stop_words,​ candidate):
 +    for i in range(len(stop_words)):​
 +        if stop_words[i] == candidate:
 +            return True
 +    return False
 +</​code>​
 +</​details>​
 +
 +
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +<?php
 +
 +$stopWords = ['​ее',​ '​на',​ '​по',​ '​со',​ '​же',​ '​браво',​ '​всего',​ '​я',​ '​итого'​];​
 +
 +function isStopWord($stopWords,​ $candidate)
 +{
 +  foreach ($stopWords as $word) {
 +    if ($word === $candidate) {
 +      return true;
 +    }
 +  }
 +
 +  return false;
 +}
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +class App {
 +
 +    private static List<​String>​ stopWords = List.of(
 +        "​ее",​ "​на",​ "​по",​ "​со",​ "​же",​ "​браво",​ "​всего",​ "​я",​ "​итого"​
 +    );
 +
 +    public static boolean isStopWord(String candidate) {
 +        for(String word : stopWords) {
 +            if (word.equals(candidate)) {
 +                return true;
 +            }
 +        }
 +
 +        return false;
 +    }
 +}
 +</​code>​
 +</​details>​
 +
 +Функция ''​isStopWord()''​ перебирает **слова-кандидаты**,​ последовательно сравнивает их с каждой строкой в массиве стоп-слов и возвращает ''​true''​ или ''​false''​. Мы вставили сюда слова из нашей таблицы,​ но, чтобы код не получился слишком большим,​ ограничились десятком слов.
 +
 +Разберемся,​ почему такой поиск работает медленно. На каждом шаге функция двигается к соседнему слову в массиве,​ чтобы его проверить. В худшем случае она проверяет все слова.
 +
 +Чтобы ускорить работу,​ нужно отбрасывать куски массива,​ где нужного слова точно нет. Пока это невозможно:​ элементы массива расположены в произвольном порядке,​ поэтому нужное нам слово может быть где угодно.
 +
 +Другое дело упорядоченный массив:​ в нем слова отсортированы по алфавиту. Мы можем не перебирать все элементы,​ а постепенно отбрасывать лишние фрагменты массива и быстрее искать стоп-слова. В этом поможет **бинарный поиск**.
 +
 +===== Бинарный поиск =====
 +
 +Бинарный поиск — это метод поиска,​ при котором алгоритм ищет элементы в ограниченной области поиска,​ причем с каждым шагом область поиска делится на две части.
 +
 +Кратко опишем механизм работы бинарного поиска:​
 +
 +  * Бинарный поиск начинается со среднего элемента и на первом шаге идет по всему массиву ​
 +  * На каждом шаге область поиска делится пополам,​ при этом границы поиска задаются первым и последним элементом ​
 +  * Алгоритм завершается,​ когда область поиска сужается до одного элемента ​
 +
 +Теперь посмотрим,​ как выглядит бинарный поиск в нашем примере со стоп-словами.
 +
 +Алгоритм начинается с середины — это слово «который».
 +
 +Очевидно,​ что перед ним нет слов «предельно» или «чтобы» — ведь буквы П и Ч в алфавите следуют за К. Если ищем слово «очень»,​ то можем отбросить все слова перед «который» — это практически половина словаря.
 +
 +Мы определили,​ что слово-кандидат может находиться только во второй половине списка. Рядом с К находятся буквы Л и М, так что поблизости от «который» могут быть только слова «лишь» и «мне». Буква О стоит далеко,​ поэтому и слово «очень» надо искать далеко. Так что заглядываем далеко вперед и попадаем,​ скажем,​ на слово «потом».
 +
 +Ситуация со словом «который» повторяется. Теперь О находится перед П, так что мы можем отбросить все слова после «потом».
 +
 +Если мы отбросим все слова перед «который» и после «потом»,​ в нашем распоряжении останется всего четверть от первоначального списка:​
 +
 +| который | ого |
 +| лишь | огромный |
 +| максимально | ой |
 +| меня | он |
 +| минимально | они |
 +| мне | от |
 +| мы | очень |
 +| на | по |
 +| над | под |
 +| не | позже |
 +| нет | после |
 +| но | |
 +
 +Теперь у нас есть **область поиска**. Сначала в область поиска входит весь список,​ но на каждом шаге она сокращается вдвое. Область поиска можно задать,​ указав на первое и последнее слово.
 +
 +Алгоритм заканчивает работу,​ когда область поиска сужается до одного слова. Если это наше слово-кандидат,​ значит поиск завершился успешно.
 +
 +==== Как реализовать бинарный поиск ====
 +
 +Выше мы описали алгоритм — осталось только превратить его в программу. Формализуем каждый шаг так, чтобы его мог выполнить компьютер.
 +
 +Вот функция,​ которая проверяет,​ есть ли стоп-слово в отсортированном массиве:​
 +
 +<code javascript>​
 +const stopWords = ['​а',​ '​без',​ '​ближе',​ '​браво',​ '​бы',​ '​вам',​ '​вас',​ '​весь',​ '​во',​ '​все',​ '​всего',​ '​вы'​];​
 +
 +const isStopWord = (candidate) => {
 +  // индекс первого элемента области поиска
 +  let first = 0;
 +  // индекс последнего элемента области поиска
 +  let last = stopWords.length - 1;
 +
 +  // пока область поиска не пуста
 +  while (first <= last) {
 +    // индекс среднего элемента области поиска
 +    const middle = Math.floor((first + last) / 2);
 +
 +    if (candidate === stopWords[middle]) {
 +      // если нашли слово-кандидат,​ поиск завершается удачно
 +      return true;
 +    }
 +
 +    if (candidate < stopWords[middle]) {
 +      // если кандидат меньше,​ отбрасываем правую половину
 +      last = middle - 1;
 +    } else {
 +      // если кандидат больше,​ отбрасываем левую половину
 +      first = middle + 1;
 +    }
 +  }
 +
 +  return false;
 +};
 +</​code>​
 +
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +stop_words = ['​а',​ '​без',​ '​ближе',​ '​браво',​ '​бы',​ '​вам',​ '​вас',​ '​весь',​ '​во',​ '​все',​ '​всего',​ '​вы'​]
 +
 +def is_stop_word(stop_words,​ candidate):
 +    # индекс первого элемента области поиска
 +    first = 0
 +    # индекс последнего элемента области поиска
 +    last = len(stop_words) - 1
 +
 +    while (first <= last):
 +        # индекс среднего элемента области поиска
 +        middle = (first + last) // 2
 +
 +        if candidate == stop_words[middle]:​
 +            # если нашли слово-кандидат,​ поиск завершается удачно
 +            return True
 +
 +        if candidate < stop_words[middle]:​
 +            # если кандидат меньше,​ отбрасываем правую половину
 +            last = middle - 1
 +        else:
 +            # если кандидат больше,​ отбрасываем левую половину
 +            first = middle + 1
 +
 +    return False
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +<?php
 +
 +$stopWords = ['​а',​ '​без',​ '​ближе',​ '​браво',​ '​бы',​ '​вам',​ '​вас',​ '​весь',​ '​во',​ '​все',​ '​всего',​ '​вы'​];​
 +
 +function isStopWord($candidate)
 +{
 +    $first = 0;
 +    $last = count($stopWords) - 1;
 +
 +    while ($first <= $last) {
 +        $middle = round(($first + $last) / 2);
 +
 +        if ($candidate === $stopWords[$middle]) {
 +            return true;
 +        }
 +
 +        if ($candidate < $stopWords[$middle]) {
 +            $last = $middle - 1;
 +        } else {
 +            $first = $middle + 1;
 +        }
 +    }
 +
 +    return false;
 +};
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +class App {
 +
 +    private static List<​String>​ stopWords = List.of(
 +        "​а",​ "​без",​ "​ближе",​ "​браво",​ "​бы",​ "​вам",​ "​вас",​ "​весь",​ "​во",​ "​все",​ "​всего",​ "​вы"​
 +    );
 +
 +    public static boolean isStopWord(String candidate) {
 +        // индекс первого элемента области поиска
 +        var first = 0;
 +        // индекс последнего элемента области поиска
 +        var last = stopWords.size() - 1;
 +
 +        // пока область поиска не пуста
 +        while (first <= last) {
 +            // индекс среднего элемента области поиска
 +            var middle = (first + last) / 2;
 +
 +            if (candidate.equals(stopWords.get(middle))) {
 +                // если нашли слово-кандидат,​ поиск завершается удачно
 +                return true;
 +            }
 +
 +            if (candidate.compareTo(stopWords.get(middle)) < 0) {
 +                // если кандидат меньше,​ отбрасываем правую половину
 +                last = middle - 1;
 +            } else {
 +                // если кандидат больше,​ отбрасываем левую половину
 +                first = middle + 1;
 +            }
 +        }
 +
 +        return false;
 +    }
 +}
 +</​code>​
 +</​details>​
 +
 +
 +Разберемся,​ как эта функция работает. На каждом шаге алгоритма мы взаимодействуем с областью поиска. Чтобы определить ее, нам достаточно хранить индексы его первого и последнего элементов. В самом начале область поиска совпадает со всем массивом.
 +
 +<code javascript>​
 +// индекс первого элемента области поиска
 +let first = 0;
 +// индекс последнего элемента области поиска
 +let last = stopWords.length - 1;
 +</​code>​
 +
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +# индекс первого элемента области поиска
 +first = 0
 +# индекс последнего элемента области поиска
 +last = len(stop_words) - 1
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +<?php
 +
 +$first = 0;
 +$last = count(stopWords) - 1;
 +</​code>​
 +</​details>​
 +
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +// индекс первого элемента области поиска
 +var first = 0;
 +// индекс последнего элемента области поиска
 +var last = stopWords.size() - 1;
 +</​code>​
 +</​details>​
 +
 +Элементы в массивах JavaScript нумеруются с нуля, поэтому сначала индекс первого элемента равен 0, а индекс последнего — на единицу меньше длины массива. Чтобы убедиться в этом, представьте массив,​ например,​ из четырех элементов:​ его элементы будут иметь индексы 0, 1, 2 и 3.
 +
 +На каждом шаге мы сравниваем слово-кандидат со словом из середины области. Найти среднее слово несложно:​ его индекс находится посередине между первым и последним словом.
 +
 +<code javascript>​
 +const middle = (first + last) / 2;
 +</​code>​
 +
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +middle = (first + last) / 2
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +<?php
 +
 +$middle = ($first + $last) / 2
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +var middle = (first + last) / 2;
 +</​code>​
 +</​details>​
 +
 +В массиве с нечетным количеством элементов все просто:​ у нас есть центральный элемент и две равные половины слева и справа от него. В массиве с четным количеством элементов,​ длина левой и правой половин будет отличаться на единицу.
 +
 +В нашей формуле это будет означать,​ что ''​first + last''​ не делится нацело на 2.
 +
 +Чтобы справиться с этой ситуацией,​ мы округлим это значение вверх или вниз. Если вниз — левая половина будет на один элемент короче правой,​ а если вверх — наоборот. Можно выбрать любой вариант,​ алгоритм будет работать в обоих случаях. Округлим значение в меньшую сторону:​
 +
 +<code javascript>​
 +// индекс среднего элемента области поиска
 +const middle = Math.floor((first + last) / 2);
 +</​code>​
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +middle = (first + last) // 2
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +$middle = round(($first + $last) / 2);
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +// В Java для целых чисел используется целочисленное деление
 +var middle = (first + last) / 2;
 +</​code>​
 +</​details>​
 +
 +Дальше есть три варианта развития событий.
 +
 +**Вариант 1:** Слово-кандидат может совпадать со средним словом — поиск завершен с положительным результатом.
 +
 +**Вариант 2:** Слово-кандидат может быть меньше слова из середины. Тогда надо отбросить все элементы справа и продолжать поиск только в левой части. На следующем шаге первое слово останется прежним,​ а последнее изменится. Последним словом в новой области поиска станет центральное слово из старой:​
 +
 +<code javascript>​
 +last = middle;
 +</​code>​
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +last = middle
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +<?php
 +
 +$last = $middle;
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +last = middle;
 +</​code>​
 +<​details>​
 +
 +На самом деле мы можем не включать центральное слово в новую область. При проверке первого варианта мы убедились,​ что слово-кандидат с ним не совпадает. Новым последним словом будет то, которое находится слева от центрального: ​
 +
 +<code javascript>​
 +last = middle - 1;
 +</​code>​
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +last = middle - 1
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +<?php
 +
 +$last = $middle - 1;
 +</​code>​
 +<​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +last = middle - 1;
 +</​code>​
 +</​details>​
 +
 +**Вариант 3:** Слово-кандидат может быть больше центрального слова. Тогда вместо правой границы надо двигать левую. Код окажется очень похожим:​
 +
 +<code javascript>​
 +first = middle + 1;
 +</​code>​
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +
 +<code python>
 +first = middle + 1
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<​code>​
 +<?php
 +
 +$first = $middle + 1;
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +<code java>
 +first = middle + 1;
 +</​code>​
 +</​details>​
 +
 +Мы сократили область поиска вдвое, а теперь надо повторить предыдущие шаги — тогда в нашей программе появится цикл:
 +
 +<code javascript>​
 +while (???) {
 +  // индекс среднего элемента области поиска
 +  const middle = Math.floor((first + last) / 2);
 +
 +  if (candidate === stopWords[middle]) {
 +    // если нашли слово-кандидат,​ поиск завершается удачно
 +    return true;
 +  }
 +
 +  if (candidate < stopWords[middle]) {
 +    // если кандидат меньше,​ отбрасываем правую половину
 +    last = middle - 1;
 +  } else {
 +    // если кандидат больше,​ отбрасываем левую половину
 +    first = middle + 1;
 +  }
 +}
 +</​code>​
 +
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +while (???):
 +    # индекс среднего элемента области поиска
 +    middle = (first + last) // 2
 +
 +    if candidate == stop_words[middle]:​
 +        # если нашли слово-кандидат,​ поиск завершается удачно
 +        return True
 +
 +    if candidate < stop_words[middle]:​
 +        # если кандидат меньше,​ отбрасываем правую половину
 +        last = middle - 1
 +    else:
 +        # если кандидат больше,​ отбрасываем левую половину
 +        first = middle + 1
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +<code php>
 +<?php
 +
 +while (???) {
 +    // индекс среднего элемента области поиска
 +    $middle = round(($first + $last) / 2);
 +
 +    if ($candidate === $stopWords[$middle]) {
 +        // если нашли слово-кандидат,​ поиск завершается удачно
 +        return true;
 +    }
 +
 +    if ($candidate < $stopWords[$middle]) {
 +        // если кандидат меньше,​ отбрасываем правую половину
 +        $last = $middle - 1;
 +    } else {
 +        // если кандидат больше,​ отбрасываем левую половину
 +        $first = $middle + 1;
 +    }
 +}
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +
 +<code java>
 +while (???) {
 +    // индекс среднего элемента области поиска
 +    var middle = (first + last) / 2;
 +
 +    if (candidate.equals(stopWords.get(middle))) {
 +        // если нашли слово-кандидат,​ поиск завершается удачно
 +        return true;
 +    }
 +
 +    if (candidate.compareTo(stopWords.get(middle)) < 0) {
 +        // если кандидат меньше,​ отбрасываем правую половину
 +        last = middle - 1;
 +    } else {
 +        // если кандидат больше,​ отбрасываем левую половину
 +        first = middle + 1;
 +    }
 +    }
 +</​code>​
 +</​details>​
 +
 +Мы пока не знаем условия завершения цикла, поэтому написали три знака вопроса. Чтобы разобраться с условием,​ обратим внимание,​ что на каждом шаге первое и последнее слово двигаются к друг другу и когда-нибудь они совпадут. Тогда условие ''​first === last''​ станет истинным.
 +
 +Если ''​first === last'',​ то формулу ''​(first + last) / 2''​ можно записать двумя способами:​
 +
 +  * ''​(first + first) / 2'' ​
 +  * ''​(last + last) / 2'' ​
 +
 +Индекс центрального элемента в любом случае будет равен ''​first''​ и ''​last''​.
 +
 +Слово-кандидат либо будет совпадать с центральным словом,​ либо нет. Если оно окажется меньше,​ у нас выполнится ветка:
 +
 +<code javasript>​
 +last = middle - 1;
 +</​code>​
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +<code python>
 +last = middle - 1
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +
 +<code php>
 +<?php
 +
 +$last = $middle - 1;
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +
 +<code java>
 +last = middle - 1;
 +</​code>​
 +</​details>​
 +
 +Мы помним,​ что значения ''​first'',​ ''​last''​ и ''​middle''​ совпадают,​ так что новое значение ''​last''​ окажется на единицу меньше ''​first''​. Ситуация кажется странной — как последнее слово может находиться перед первым?​
 +
 +Так бывает в **вырожденных случаях** — массивах без элементов для сравнения и с пустой областью поиска. При последнем сравнении слово-кандидат не совпало со словом из списка — значит,​ его вообще не было в списке.
 +
 +Такая же ситуация возникнет,​ если слово-кандидат будет больше среднего слова, но там сработает вторая ветка:
 +
 +<code javascript>​
 +first = middle + 1;
 +</​code>​
 +
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +
 +<code python>
 +first = middle + 1
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +
 +<code php>
 +<?php
 +
 +$first = $middle + 1;
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +
 +<code java>
 +first = middle + 1;
 +</​code>​
 +</​details>​
 +
 +Цикл следует остановить,​ когда левая граница станет больше правой. Но в операторе ''​while''​ мы записываем не условие остановки цикла, а условие продолжения,​ поэтому условие надо поменять на обратное:​ ''​first <= last''​. Если цикл завершается и кандидат в списке не найден,​ значит,​ поиск завершился неудачно — алгоритм вернет ''​false'':​
 +
 +<code javascript> ​
 +// пока область поиска не пуста
 +while (first <= last) {
 +  // ...
 +}
 +
 +return false;
 +</​code>​
 +
 +
 +<​details>​
 +<​summary>​Python</​summary>​
 +
 +<code python>
 +# пока область поиска не пуста
 +while first <= last:
 +    # ...
 +
 +return False
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​PHP</​summary>​
 +
 +<code php>
 +<?php
 +
 +// пока область поиска не пуста
 +while ($first <= $last) {
 +    // ...
 +}
 +
 +return false;
 +</​code>​
 +</​details>​
 +
 +<​details>​
 +<​summary>​Java</​summary>​
 +
 +<code java>
 +// пока область поиска не пуста
 +while (first <= last) {
 +    // ...
 +}
 +</​code>​
 +</​details>​
 +
 +==== Недостатки бинарного поиска ====
  
 На примере списка стоп-слов сравним метод перебора и бинарный поиск. Первое заметное отличие — **бинарный поиск сложнее в реализации**. В нашем случае сортировка метода перебора занимает 8 строк, а бинарный поиск — 18 строк. Стоит ли писать такие сложные программы?​ Да, если мы ищем по массиву из ста элементов и более. На примере списка стоп-слов сравним метод перебора и бинарный поиск. Первое заметное отличие — **бинарный поиск сложнее в реализации**. В нашем случае сортировка метода перебора занимает 8 строк, а бинарный поиск — 18 строк. Стоит ли писать такие сложные программы?​ Да, если мы ищем по массиву из ста элементов и более.
Строка 12: Строка 771:
 И третье ограничение — некоторые данные просто нельзя упорядочить. Например,​ не существует какого-то естественного общепризнанного способа упорядочить цвета: И третье ограничение — некоторые данные просто нельзя упорядочить. Например,​ не существует какого-то естественного общепризнанного способа упорядочить цвета:
  
-{{https://cdn2.hexlet.io/​derivations/​image/​original/​eyJpZCI6IjNlY2RiNmFmZTNiZjU5NGJhYTE3YWQwNTI4ODJhZTJhLnBuZyIsInN0b3JhZ2UiOiJjYWNoZSJ9|Цвета}} Таким же образом,​ сложно упорядочить пары чисел. Возьмем для примера широту и долготу — это как раз пара чисел:+{{ :basics_of_algorithms:​image_processing20231003-35-vql2f3.png |}}
  
-  ​* Самый западный город России — Балтийск,​ его координаты 54°39′ с. ш. 19°55′ в. д. +Таким же образом,​ сложно упорядочить пары чисел. Возьмем для примера широту и долготу — это как раз пара чисел:​ 
-  * Самый северный — Певек, координаты 69°42′ с. ш. 170°19′ в. д.+ 
 +  ​* Самый западный город России — Балтийск,​ его координаты 54°39′ с. ш. 19°55′ в. д.  
 +  * Самый северный — Певек, координаты 69°42′ с. ш. 170°19′ в. д. 
  
 Какие координаты больше?​ Если бы речь шла только о широте или долготе,​ мы бы без труда ответили. Но здесь пара чисел: при сравнении широта может оказаться больше,​ а долгота меньше. Нельзя сказать,​ что широта важнее долготы или наоборот. Поэтому у координат и других пар чисел нет какого-то естественного и очевидного порядка. Какие координаты больше?​ Если бы речь шла только о широте или долготе,​ мы бы без труда ответили. Но здесь пара чисел: при сравнении широта может оказаться больше,​ а долгота меньше. Нельзя сказать,​ что широта важнее долготы или наоборот. Поэтому у координат и других пар чисел нет какого-то естественного и очевидного порядка.
Строка 37: Строка 798:
 Лучшим для бинарного поиска также окажется один цикл. В худшем случае бинарный поиск пройдет четыре цикла: Лучшим для бинарного поиска также окажется один цикл. В худшем случае бинарный поиск пройдет четыре цикла:
  
-  * Первый поиск — целый массив из 10 элементов +  * Первый поиск — целый массив из 10 элементов  
-  * Второй — подмассив из пяти элементов +  * Второй — подмассив из пяти элементов  
-  * Третий — подмассив из двух элементов +  * Третий — подмассив из двух элементов  
-  * Четвертый — подмассив с одним последним элементом+  * Четвертый — подмассив с одним последним элементом ​
  
 В итоге среднее число циклов для бинарного поиска — 2. В итоге среднее число циклов для бинарного поиска — 2.
Строка 47: Строка 808:
  
 ^Размер^Перебор,​ среднее^Бинарный поиск, среднее| ^Размер^Перебор,​ среднее^Бинарный поиск, среднее|
-|10|5|2| +| 10 | 5 | 2 | 
-|1 000|500|5| +| 1 000 | 500 | 5 | 
-|1 000 000|500 000|10|+| 1 000 000 | 500 000 | 10 |
  
 Как видим, разница в производительности колоссальная,​ особенно на больших массивах. Как видим, разница в производительности колоссальная,​ особенно на больших массивах.
Строка 55: Строка 816:
 ===== Заключение ===== ===== Заключение =====
  
-  * В уроке мы рассмотрели два алгоритма:​ метод перебора и бинарный поиск +  * В уроке мы рассмотрели два алгоритма:​ метод перебора и бинарный поиск  
-  * У метода перебора есть два варианта:​ Простой перебор,​ чтобы проверить,​ есть ли элемент в списке +  * У метода перебора есть два варианта:​ Простой перебор,​ чтобы проверить,​ есть ли элемент в списке ​
- +
-===== Для полного доступа к курсу нужен базовый план ===== +
- +
-Базовый план откроет полный доступ ко всем курсам,​ упражнениям и урокам Хекслета,​ проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент. +
- +
-[[:​subscription:​new?​nickname=professional_monthly_3900|Получить доступ]] +
- +
----- +
- +
-130 +
- +
-[[:​courses|курсов]] +
- +
-1000 +
- +
-упражнений +
- +
-2000 +
- +
-часов теории +
- +
-3200 +
- +
-тестов +
- +
-\\+
  
  
basics_of_algorithms/binary_search.1696349239.txt.gz · Последние изменения: 2023/10/03 19:07 — werwolf