Бинарный поиск: различия между версиями

Материал из Олимпиадное программирование в УлГТУ
Перейти к навигации Перейти к поиску
Нет описания правки
 
(не показано 10 промежуточных версий этого же участника)
Строка 1: Строка 1:
== TLDR ==
<youtube width="300" height="180">q06xEZ7coR0</youtube>
<youtube width="300" height="180">DkPjtl5fkks</youtube>
<youtube width="300" height="180">_u2yypDA6R0</youtube>
<youtube width="300" height="180">mvC_9pKG3uQ</youtube>
<youtube width="300" height="180">rmWhWsPg2MA</youtube>
<youtube width="300" height="180">8WE6cIrqb6M</youtube>
== Бинарный поиск элемента в отсортированном массиве ==
Рассмотрим задачу: дан массив a[], состоящий из целых чисел, и требуется найти в нём элемент x. Если x присутствует в a, то нужно вернуть его индекс (если x встречается несколько раз, то можно вернуть любой из подходящих индексов), иначе, если x нет, — вернуть -1.
Простейший способ решить такую задачу — пройти по всем элементам массива и сравнить их с x:
int search(int a[], int size, int x) { // ищем x в массиве a размера size
    for (int i = 0; i < size; i++)    // перебираем все элементы массива
        if (a[i] == x)                // если элемент равен x,
            return i;                  // возвращаем его индекс
    return -1;                        // если не встретили x, возвращаем -1
}
Такое решение называют линейным поиском. Очевидно, что количество действий, которые данный алгоритм выполняет в худшем случае (когда x отсутствует в массиве), пропорционально размеру массива (поиск выполняется за O(N)).
Если массив отсортирован, то мы можем использовать более эффективную стратегию. Пусть массив упорядочен по возрастанию, тогда:
* Найдём центральный элемент массива и сравним его с x. Если он равен x, то ответ найден;
* Если центральный элемент меньше x, то искать ответ имеет смысл только в правой половине массива (так как слева все элементы тоже меньше x);
* Если центральный элемент больше x, то искать ответ имеет смысл только в левой половине массива (так как справа все элементы тоже больше x);
* При переходе в ту или иную половину мы снова определяем в ней средний элемент и сравниваем его с x, и так далее, пока ответ не будет найден или пока в рассматриваемой области не останется элементов.
Для того, чтобы отслеживать текущую область поиска, заведём индексы l и r. Будем считать, что поиск производится в отрезке от l до r включительно; таким образом, изначально l = 0, r = size - 1. Зная l и r, середину можно найти как (l + r) / 2 или l + (r - l) / 2.
int binarySearch(int a[], int size, int x) { // ищем x в отсортированном массиве a размера size
    int l = 0, r = size - 1;                // ищем в отрезке [l; r]. изначально это весь массив
    while (l <= r) {                        // пока в отрезке поиска есть хотя бы один элемент,
        int m = l + (r - l) / 2;            // находим индекс среднего элемента отрезка
        if (a[m] == x)                      // если средний элемент равен x,
            return m;                        // возвращаем его индекс
        else if (a[m] < x)                  // если средний элемент меньше x,
            l = m + 1;                      // продолжаем искать в правой половине - [m + 1; r]
        else                                // если средний элемент больше x,
            r = m - 1;                      // продолжаем искать в левой половине - [l; m - 1]
    }
    return -1                                // если не встретили x, возвращаем -1.
}
Такое решение называют бинарным поиском. Бинарный поиск '''гораздо''' быстрее линейного, так на каждой итерации он сокращает область поиска в два раза (поиск выполняется за O(logN)).
{| class="wikitable"
|+Число сравнений в худшем случае
| width="100px" | Размер массива
| width="100px" | Линейный поиск
| width="100px" | Бинарный поиск
|-
| 10
| 10
| 4
|-
| 100
| 100
| 7
|-
| 1000
| 1000
| 10
|-
| 10<sup>6</sup>
| 10<sup>6</sup>
| 20
|-
| 10<sup>9</sup>
| 10<sup>9</sup>
| 30
|}
Для бинарного поиска массив должен быть отсортирован, в общем случае сортировка требует времени O(NlogN).
* Если элемент ищется лишь один раз, а массив не отсортирован, то выгоднее использовать линейный поиск (O(N) выгоднее, чем O(NlogN) + O(logN));
* Если элементы ищутся много раз, то выгоднее отсортировать массив и использовать бинарный поиск (O(NlogN) + много O(logN) выгоднее, чем много O(N));
* Если элементы добавляются и удаляются, то вместо отсортированного массива следует использовать другую коллекцию — двоичное дерево поиска или хеш-таблицу;
* При помощи хеш-таблицы можно решать исходную задачу эффективнее, чем двоичным поиском, — за константное время (O(1)). Но идея двоичного поиска применима в существенно более широком наборе задач, как мы увидим далее.
== Левый и правый бинарный поиск ==
Рассмотренная выше задача, в которой требуется найти любое вхождение заданного элемента в массив, встречается сравнительно редко и представляет мало интереса. Гораздо важнее другой вид задач: дан отсортированный массив a[] и требуется найти в нём индекс '''первого''' или '''последнего''' вхождения числа x (или индекс последнего элемента, меньшего x, или индекс первого элемента, большего x).
При решении данной задачи мы уже не можем сразу вернуть индекс, как только нашли элемент, равный x, и функцию поиска придётся усложнить.
Оказывается, что написание бинарного поиска первого или последнего вхождения — достаточно трудная задача. Программист вынужден держать в голове множество тонких моментов. Вот некоторые из них:
* Является ли диапазон от l до r отрезком или полуинтервалом, как он отсортирован, содержит ли он искомый элемент;
* Как следует проверять центральный элемент и как смещать границы: l = m или l = m + 1, r = m или r = m - 1;
* Будет ли поиск правильно работать на массивах из 0, 1, 2 элементов;
* Какой из индексов в итоге указывает на ответ, как проверить, что ответ отсутствует, и др.
=== Правила написания бинарного поиска без головной боли ===
* Диапазон от l до r — всегда отрезок (включительно, [l; r]), изначально l = 0, r = size - 1. Искомый элемент изначально может как быть в отрезке, так и не быть, это не важно;
* Поиск всегда осуществляется до двух элементов (while (l + 1 < r));
* После проверки среднего элемента границы всегда смещаются только на середину (l = m или r = m, никаких плюс-минус единиц);
* Смещение границ должно происходить так, чтобы не потерять искомый элемент (чтобы он не выпал из отрезка [l; r], если он там был).
: Пример: массив отсортирован по неубыванию, ищем первый элемент, равный x. Простые, очевидные случаи: если a[m] < x, то l = m; если a[m] > x, то r = m. Более важный и трудный случай: a[m] == x. Если мы сдвинем левую границу, то можем потерять первый x, а если сдвинем правую — не потеряем. Поэтому нужно двигать правую: r = m.
: Подобным же образом нужно рассуждать во всех аналогичных задачах. Запоминать условия не нужно.
* Когда цикл завершится, останутся два соседних элемента — a[l] и a[r]. Их нужно проверить по отдельности. При этом, если мы ищем первое вхождение чего-либо, то сначала проверяем a[l], затем a[r]; если ищем последнее вхождение — сначала a[r], затем a[l].
Следуя этим правилам, вы сможете практически единообразно писать различные виды левых и правых двоичных поисков. Сравните показанные ниже примеры: в них меняются только условие проверки среднего элемента (выделено красным) и порядок проверки последних двух элементов (выделено оранжевым).
{|
|
// сорт. по неубыванию
// последний элемент < x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] < x</span>)
    l = m;
  else
    r = m;
}
<span style="color:orange">if (a[r] < x)</span>
  <span style="color:orange">return r;</span>
<span style="color:orange">if (a[l] < x)</span>
  <span style="color:orange">return l;</span>
return -1;
|
// сорт. по неубыванию
// последний элемент <= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] <= x</span>)
    l = m;
  else
    r = m;
}
<span style="color:orange">if (a[r] <= x)</span>
  <span style="color:orange">return r;</span>
<span style="color:orange">if (a[l] <= x)</span>
  <span style="color:orange">return l;</span>
return -1;
|
// сорт. по неубыванию
// последний элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] <= x</span>)
    l = m;
  else
    r = m;
}
<span style="color:orange">if (a[r] == x)</span>
  <span style="color:orange">return r;</span>
<span style="color:orange">if (a[l] == x)</span>
  <span style="color:orange">return l;</span>
return -1;
|
// сорт. по неубыванию
// первый элемент > x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] <= x</span>) // if (<span style="color:red">a[m] > x</span>)
    l = m;      //  r = m;
  else          // else
    r = m;      //  l = m;
}
<span style="color:orange">if (a[l] > x)</span>
  <span style="color:orange">return l;</span>
<span style="color:orange">if (a[r] > x)</span>
  <span style="color:orange">return r;</span>
return -1;
|
// сорт. по неубыванию
// первый элемент >= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] < x</span>) // if (<span style="color:red">a[m] >= x</span>)
    l = m;      //  r = m;
  else          // else
    r = m;      //  l = m;
}
<span style="color:orange">if (a[l] >= x)</span>
  <span style="color:orange">return l;</span>
<span style="color:orange">if (a[r] >= x)</span>
  <span style="color:orange">return r;</span>
return -1;
|
// сорт. по неубыванию
// первый элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] < x</span>) // if (<span style="color:red">a[m] >= x</span>)
    l = m;      //  r = m;
  else          // else
    r = m;      //  l = m;
}
<span style="color:orange">if (a[l] == x)</span>
  <span style="color:orange">return l;</span>
<span style="color:orange">if (a[r] == x)</span>
  <span style="color:orange">return r;</span>
return -1;
|-
|
// сорт. по невозрастанию
// последний элемент > x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] > x</span>)
    l = m;
  else
    r = m;
}
<span style="color:orange">if (a[r] > x)</span>
  <span style="color:orange">return r;</span>
<span style="color:orange">if (a[l] > x)</span>
  <span style="color:orange">return l;</span>
return -1;
|
// сорт. по невозрастанию
// последний элемент >= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] >= x</span>)
    l = m;
  else
    r = m;
}
<span style="color:orange">if (a[r] >= x)</span>
  <span style="color:orange">return r;</span>
<span style="color:orange">if (a[l] >= x)</span>
  <span style="color:orange">return l;</span>
return -1;
|
// сорт. по невозрастанию
// последний элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] >= x</span>)
    l = m;
  else
    r = m;
}
<span style="color:orange">if (a[r] == x)</span>
    <span style="color:orange">return r;</span>
<span style="color:orange">if (a[l] == x)</span>
    <span style="color:orange">return l;</span>
return -1;
|
// сорт. по невозрастанию
// первый элемент < x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] >= x</span>) // if (<span style="color:red">a[m] < x</span>)
    l = m;      //  r = m;
  else          // else
    r = m;      //  l = m;
}
<span style="color:orange">if (a[l] < x)</span>
  <span style="color:orange">return l;</span>
<span style="color:orange">if (a[r] < x)</span>
  <span style="color:orange">return r;</span>
return -1;
|
// сорт. по невозрастанию
// первый элемент <= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] > x</span>) // if (<span style="color:red">a[m] <= x</span>)
    l = m;      //  r = m;
  else          // else
    r = m;      //  l = m;
}
<span style="color:orange">if (a[l] <= x)</span>
  <span style="color:orange">return l;</span>
<span style="color:orange">if (a[r] <= x)</span>
  <span style="color:orange">return r;</span>
return -1;
|
// сорт. по невозрастанию
// первый элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">a[m] > x</span>) // if (<span style="color:red">a[m] <= x</span>)
    l = m;      //  r = m;
  else          // else
    r = m;      //  l = m;
}
<span style="color:orange">if (a[l] == x)</span>
  <span style="color:orange">return l;</span>
<span style="color:orange">if (a[r] == x)</span>
  <span style="color:orange">return r;</span>
return -1;
|-
|
// f() возвращает
// сначала true, потом false
// последний f() == true
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">f(a[m])</span>)
    l = m;
  else
    r = m;
}
<span style="color:orange">if (f(a[r]))</span>
  <span style="color:orange">return r;</span>
<span style="color:orange">if (f(a[l])</span>
  <span style="color:orange">return l;</span>
return -1;
|||
|
// f() возвращает
// сначала false, потом true
// первый f() == true
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (<span style="color:red">!f(a[m])</span>) // if (<span style="color:red">f(a[m])</span>)
    l = m;      //  r = m;
  else          // else
    r = m;      //  l = m;
}
<span style="color:orange">if (f(a[l])</span>
  <span style="color:orange">return l;</span>
<span style="color:orange">if (f(a[r]))</span>
  <span style="color:orange">return r;</span>
return -1;
|}
При желании [[Бинарный поиск в интервале|показанный алгоритм можно оптимизировать]], сократив количество проверок в конце.
== Ссылки ==
== Ссылки ==
Теория:
Теория:
* [https://codeforces.com/edu/course/2/lesson/6 Codeforces EDU — Двоичный поиск]
* [http://neerc.ifmo.ru/wiki/index.php?title=%D0%A6%D0%B5%D0%BB%D0%BE%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA neerc.ifmo.ru/wiki &mdash; Целочисленный двоичный поиск]
* [http://neerc.ifmo.ru/wiki/index.php?title=%D0%A6%D0%B5%D0%BB%D0%BE%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA neerc.ifmo.ru/wiki &mdash; Целочисленный двоичный поиск]
* [http://neerc.ifmo.ru/wiki/index.php?title=%D0%92%D0%B5%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA neerc.ifmo.ru/wiki &mdash; Вещественный двоичный поиск]
* [http://neerc.ifmo.ru/wiki/index.php?title=%D0%92%D0%B5%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B4%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA neerc.ifmo.ru/wiki &mdash; Вещественный двоичный поиск]
* [http://brestprog.neocities.org/lections/binsearch.html brestprog.neocities.org &mdash; Бинарный поиск]
* [http://brestprog.by/topics/binsearch/ brestprog Бинарный поиск]
* [http://algorithmica.org/tg/binary-search algorithmica.org — Бинарный поиск]
* [http://algorithmica.org/tg/binary-search algorithmica.org — Бинарный поиск]
* [http://github.com/petr-kalinin/progtexts/releases/download/v2014.11.01/07_binsearch.pdf Калинин П. &mdash; Двоичный поиск]
* [https://notes.algoprog.ru/binsearch/07_binsearch_main.html Калинин П. — Бинарный поиск]
* [http://brilliant.org/wiki/binary-search/ Brilliant.org — Binary Search]
Код:
Код:
* [http://github.com/indy256/codelibrary/blob/master/java/src/BinarySearch.java CodeLibrary &mdash; Binary search]
* [https://github.com/indy256/codelibrary/blob/master/cpp/misc/binary_search.cpp CodeLibrary Binary search]
Задачи:
Задачи:
* [http://informatics.mccme.ru/course/view.php?id=3 informatics.mccme.ru &mdash; Курс &laquo;Поиск и сортировка&raquo; &mdash; часть 2]
* [http://informatics.mccme.ru/course/view.php?id=3 informatics.mccme.ru &mdash; Курс &laquo;Поиск и сортировка&raquo; &mdash; часть 2]

Текущая версия от 14:59, 24 мая 2023

TLDR

Бинарный поиск элемента в отсортированном массиве

Рассмотрим задачу: дан массив a[], состоящий из целых чисел, и требуется найти в нём элемент x. Если x присутствует в a, то нужно вернуть его индекс (если x встречается несколько раз, то можно вернуть любой из подходящих индексов), иначе, если x нет, — вернуть -1.

Простейший способ решить такую задачу — пройти по всем элементам массива и сравнить их с x:

int search(int a[], int size, int x) { // ищем x в массиве a размера size
    for (int i = 0; i < size; i++)     // перебираем все элементы массива
        if (a[i] == x)                 // если элемент равен x,
            return i;                  // возвращаем его индекс
    return -1;                         // если не встретили x, возвращаем -1
}

Такое решение называют линейным поиском. Очевидно, что количество действий, которые данный алгоритм выполняет в худшем случае (когда x отсутствует в массиве), пропорционально размеру массива (поиск выполняется за O(N)).

Если массив отсортирован, то мы можем использовать более эффективную стратегию. Пусть массив упорядочен по возрастанию, тогда:

  • Найдём центральный элемент массива и сравним его с x. Если он равен x, то ответ найден;
  • Если центральный элемент меньше x, то искать ответ имеет смысл только в правой половине массива (так как слева все элементы тоже меньше x);
  • Если центральный элемент больше x, то искать ответ имеет смысл только в левой половине массива (так как справа все элементы тоже больше x);
  • При переходе в ту или иную половину мы снова определяем в ней средний элемент и сравниваем его с x, и так далее, пока ответ не будет найден или пока в рассматриваемой области не останется элементов.

Для того, чтобы отслеживать текущую область поиска, заведём индексы l и r. Будем считать, что поиск производится в отрезке от l до r включительно; таким образом, изначально l = 0, r = size - 1. Зная l и r, середину можно найти как (l + r) / 2 или l + (r - l) / 2.

int binarySearch(int a[], int size, int x) { // ищем x в отсортированном массиве a размера size
    int l = 0, r = size - 1;                 // ищем в отрезке [l; r]. изначально это весь массив
    while (l <= r) {                         // пока в отрезке поиска есть хотя бы один элемент,
        int m = l + (r - l) / 2;             // находим индекс среднего элемента отрезка
        if (a[m] == x)                       // если средний элемент равен x,
            return m;                        // возвращаем его индекс
        else if (a[m] < x)                   // если средний элемент меньше x,
            l = m + 1;                       // продолжаем искать в правой половине - [m + 1; r]
        else                                 // если средний элемент больше x,
            r = m - 1;                       // продолжаем искать в левой половине - [l; m - 1]
    }
    return -1                                // если не встретили x, возвращаем -1.
}

Такое решение называют бинарным поиском. Бинарный поиск гораздо быстрее линейного, так на каждой итерации он сокращает область поиска в два раза (поиск выполняется за O(logN)).

Число сравнений в худшем случае
Размер массива Линейный поиск Бинарный поиск
10 10 4
100 100 7
1000 1000 10
106 106 20
109 109 30

Для бинарного поиска массив должен быть отсортирован, в общем случае сортировка требует времени O(NlogN).

  • Если элемент ищется лишь один раз, а массив не отсортирован, то выгоднее использовать линейный поиск (O(N) выгоднее, чем O(NlogN) + O(logN));
  • Если элементы ищутся много раз, то выгоднее отсортировать массив и использовать бинарный поиск (O(NlogN) + много O(logN) выгоднее, чем много O(N));
  • Если элементы добавляются и удаляются, то вместо отсортированного массива следует использовать другую коллекцию — двоичное дерево поиска или хеш-таблицу;
  • При помощи хеш-таблицы можно решать исходную задачу эффективнее, чем двоичным поиском, — за константное время (O(1)). Но идея двоичного поиска применима в существенно более широком наборе задач, как мы увидим далее.

Левый и правый бинарный поиск

Рассмотренная выше задача, в которой требуется найти любое вхождение заданного элемента в массив, встречается сравнительно редко и представляет мало интереса. Гораздо важнее другой вид задач: дан отсортированный массив a[] и требуется найти в нём индекс первого или последнего вхождения числа x (или индекс последнего элемента, меньшего x, или индекс первого элемента, большего x).

При решении данной задачи мы уже не можем сразу вернуть индекс, как только нашли элемент, равный x, и функцию поиска придётся усложнить.

Оказывается, что написание бинарного поиска первого или последнего вхождения — достаточно трудная задача. Программист вынужден держать в голове множество тонких моментов. Вот некоторые из них:

  • Является ли диапазон от l до r отрезком или полуинтервалом, как он отсортирован, содержит ли он искомый элемент;
  • Как следует проверять центральный элемент и как смещать границы: l = m или l = m + 1, r = m или r = m - 1;
  • Будет ли поиск правильно работать на массивах из 0, 1, 2 элементов;
  • Какой из индексов в итоге указывает на ответ, как проверить, что ответ отсутствует, и др.

Правила написания бинарного поиска без головной боли

  • Диапазон от l до r — всегда отрезок (включительно, [l; r]), изначально l = 0, r = size - 1. Искомый элемент изначально может как быть в отрезке, так и не быть, это не важно;
  • Поиск всегда осуществляется до двух элементов (while (l + 1 < r));
  • После проверки среднего элемента границы всегда смещаются только на середину (l = m или r = m, никаких плюс-минус единиц);
  • Смещение границ должно происходить так, чтобы не потерять искомый элемент (чтобы он не выпал из отрезка [l; r], если он там был).
Пример: массив отсортирован по неубыванию, ищем первый элемент, равный x. Простые, очевидные случаи: если a[m] < x, то l = m; если a[m] > x, то r = m. Более важный и трудный случай: a[m] == x. Если мы сдвинем левую границу, то можем потерять первый x, а если сдвинем правую — не потеряем. Поэтому нужно двигать правую: r = m.
Подобным же образом нужно рассуждать во всех аналогичных задачах. Запоминать условия не нужно.
  • Когда цикл завершится, останутся два соседних элемента — a[l] и a[r]. Их нужно проверить по отдельности. При этом, если мы ищем первое вхождение чего-либо, то сначала проверяем a[l], затем a[r]; если ищем последнее вхождение — сначала a[r], затем a[l].

Следуя этим правилам, вы сможете практически единообразно писать различные виды левых и правых двоичных поисков. Сравните показанные ниже примеры: в них меняются только условие проверки среднего элемента (выделено красным) и порядок проверки последних двух элементов (выделено оранжевым).

// сорт. по неубыванию
// последний элемент < x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] < x)
    l = m;
  else
    r = m;
}
if (a[r] < x)
  return r;
if (a[l] < x)
  return l;
return -1;
// сорт. по неубыванию
// последний элемент <= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] <= x)
    l = m;
  else
    r = m;
}
if (a[r] <= x)
  return r;
if (a[l] <= x)
  return l;
return -1;
// сорт. по неубыванию
// последний элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] <= x)
    l = m;
  else
    r = m;
}
if (a[r] == x)
  return r;
if (a[l] == x)
  return l;
return -1;
// сорт. по неубыванию
// первый элемент > x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] <= x) // if (a[m] > x)
    l = m;       //   r = m;
  else           // else
    r = m;       //   l = m;
}
if (a[l] > x)
  return l;
if (a[r] > x)
  return r;
return -1;
// сорт. по неубыванию
// первый элемент >= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] < x) // if (a[m] >= x)
    l = m;      //   r = m;
  else          // else
    r = m;      //   l = m;
}
if (a[l] >= x)
  return l;
if (a[r] >= x)
  return r;
return -1;
// сорт. по неубыванию
// первый элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] < x) // if (a[m] >= x)
    l = m;      //   r = m;
  else          // else
    r = m;      //   l = m;
}
if (a[l] == x)
  return l;
if (a[r] == x)
  return r;
return -1;
// сорт. по невозрастанию
// последний элемент > x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] > x)
    l = m;
  else
    r = m;
}
if (a[r] > x)
  return r;
if (a[l] > x)
  return l;
return -1;
// сорт. по невозрастанию
// последний элемент >= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] >= x)
    l = m;
  else
    r = m;
}
if (a[r] >= x)
  return r;
if (a[l] >= x)
  return l;
return -1;
// сорт. по невозрастанию
// последний элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] >= x)
    l = m;
  else
    r = m;
}
if (a[r] == x)
    return r;
if (a[l] == x)
    return l;
return -1;
// сорт. по невозрастанию
// первый элемент < x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] >= x) // if (a[m] < x)
    l = m;       //   r = m;
  else           // else
    r = m;       //   l = m;
}
if (a[l] < x)
  return l;
if (a[r] < x)
  return r;
return -1;
// сорт. по невозрастанию
// первый элемент <= x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] > x) // if (a[m] <= x)
    l = m;      //   r = m;
  else          // else
    r = m;      //   l = m;
}
if (a[l] <= x)
  return l;
if (a[r] <= x)
  return r;
return -1;
// сорт. по невозрастанию
// первый элемент == x
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (a[m] > x) // if (a[m] <= x)
    l = m;      //   r = m;
  else          // else
    r = m;      //   l = m;
}
if (a[l] == x)
  return l;
if (a[r] == x)
  return r;
return -1;
// f() возвращает 
// сначала true, потом false
// последний f() == true
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (f(a[m]))
    l = m;
  else
    r = m;
}
if (f(a[r]))
  return r;
if (f(a[l])
  return l;
return -1;
// f() возвращает 
// сначала false, потом true
// первый f() == true
int l = 0, r = size - 1;
while (l + 1 < r) {
  int m = l + (r - l) / 2;
  if (!f(a[m])) // if (f(a[m])) 
    l = m;      //   r = m;
  else          // else
    r = m;      //   l = m;
}
if (f(a[l])
  return l;
if (f(a[r]))
  return r;
return -1;

При желании показанный алгоритм можно оптимизировать, сократив количество проверок в конце.

Ссылки

Теория:

Код:

Задачи: