{ }codelper

Learn › TypeScript

Algorithms · 28

Binary Search

Throw away half of a sorted range at a time to find a value, or the first position that meets a condition.

Binary search looks at the middle of a sorted list and throws away the half that cannot hold the answer, again and again.
The example below has scores sorted from low to high, and for each queried score x it finds the first position whose value is x or more.
That position is also the number of scores below x, so it prints that number and then yes if someone scored exactly x, or no if not.

import { readFileSync } from "node:fs"; function lowerBound(a: number[], x: number): number {  let lo = 0, hi = a.length; // 답은 lo부터 hi까지 가운데 어딘가  while (lo < hi) {    const mid: number = Math.floor((lo + hi) / 2);    if (a[mid] < x) lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요    else hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요  }  return lo; // x 이상인 첫 자리(없으면 a.length)} const lines: string[] = readFileSync(0, "utf8").split("\n");const [n] = lines[0].split(" ").map(Number);const scores: number[] = lines[1].trim().split(" ").map(Number);for (const x of lines[2].trim().split(" ").map(Number)) {  const pos: number = lowerBound(scores, x); // x보다 낮은 점수의 개수이기도 해요  const found: boolean = pos < n && scores[pos] === x;  console.log(pos, found ? "yes" : "no");}
stdin
7 5
40 55 55 70 82 82 95
55 60 82 100 10
stdout
1 yes
3 no
4 yes
7 no
0 no

Key points

  • When a list is sorted, one look at the middle rules out half of it.
    If the middle is less than x, everything to its left is less than x too; if the middle is x or more, so is everything to its right.
  • The example's lower_bound returns the first position whose value is x or more.
    If the value there equals x, then x is present, and among equal values it is the first one.
    If there is no such value, it returns the length n.
  • One promise holds through the whole loop: everything left of lo is less than x, and everything from hi onward is x or more.
    At the start (lo = 0, hi = n) both sides are empty, so it is true, and lo = mid + 1 and hi = mid never break it.
    So the place where lo and hi meet is the answer.
  • While lo < hi, mid is always less than hi.
    So whether you do lo = mid + 1 or hi = mid, the range shrinks by at least one, and the loop must end.
  • The range halves every time, so even a million items need at most 20 looks.
    Measured on our judge with 10,000 scores and 10,000 queries: a Python solution that counts from the front took over 3 seconds, and binary search took about 0.02 s.
  • It is not only for finding values.
    For any condition that is false for a while and then true from some position on, the same method finds the first position where the condition is true.
    In the example the condition is "the value is x or more".

How each language writes it

Pythonmid = (lo + hi) // 2.
Built in: import bisect — bisect.bisect_left(a, x) is the first position with x or more (the same as the example's function), bisect.bisect_right(a, x) is the first position greater than x
JavaScriptconst mid = Math.floor((lo + hi) / 2); — / gives a fraction, so always round down.
There is no built-in binary search, so write it yourself
TypeScriptSame as JavaScript
Javaint mid = lo + (hi - lo) / 2;.
The built-in Arrays.binarySearch(a, x) returns the position if found and -(insertion point) - 1 if not (60 in the example's scores gives -4).
With equal values there is no promise which of them you get (here 82 gives 5, not the first one, 4).
For lists use Collections.binarySearch
C#int mid = lo + (hi - lo) / 2;.
The built-in Array.BinarySearch(a, x) returns a negative number if not found, and ~ turns it into the insertion point (60 gives -4, and ~(-4) is 3).
With equal values it may not be the first one (82 gives 5).
For lists use list.BinarySearch(x)
C++std::lower_bound(v.begin(), v.end(), x) - v.begin() from #include <algorithm> is the same as the example's function.
The first position greater than x is std::upper_bound; to learn only whether x is present, std::binary_search
Cint mid = lo + (hi - lo) / 2;.
bsearch(&x, a, n, sizeof a[0], cmp) from <stdlib.h> returns only the address of an equal value or NULL, so it cannot find "the first position with x or more".
Write the loop yourself as in the example
Craykonmid = Mathf.Quot(lo + hi, 2); — / gives a fraction, and a[1.5] stops with "Use a whole-number index inside [ ]".
There is no built-in binary search, so write it yourself

Common mistakes

Narrowing with lo = mid never ends

a = [10, 20, 30]
lo, hi = 0, 2  # find the last position with 20 or less
while lo < hi:
    mid = (lo + hi) // 2
    if a[mid] <= 20:
        lo = mid
    else:
        hi = mid - 1
print(lo)

→ It never ends. At lo = 1, hi = 2, mid is 1 again and the range stops shrinking

In the form that uses lo = mid, round the middle up (mid = (lo + hi + 1) // 2).
Then it prints 1 and ends.
Rather than mixing forms, it is safer to learn the one form in the example (lo = mid + 1, hi = mid).

Computing the middle with just / 2JavaScript · TypeScript

const a = [10, 20, 30];
let lo = 0, hi = a.length;
while (lo < hi) {
  const mid = (lo + hi) / 2;
  if (a[mid] < 25) lo = mid + 1;
  else hi = mid;
}
console.log(lo);

→ 1 — the answer is 2. The first mid is 1.5 and a[1.5] is undefined with no error, so after 1,077 laps it hands back the wrong position

Round down with Math.floor((lo + hi) / 2).
Then it prints 2.

(lo + hi) / 2 overflowedJava · C# · C · C++

int lo = 1500000000, hi = 2000000000;
int mid = (lo + hi) / 2;
System.out.println(mid);

→ -397483648 — lo + hi went past the end of int (about 2.1 billion) and turned negative. C# gives the same value. In C and C++, overflowing an int is undefined behaviour: nothing is promised about what happens. Our judge happened to print the same value, but do not count on it

Write int mid = lo + (hi - lo) / 2; and you get 1750000000.
It is rare with array indexes, but you meet it at once when halving a range such as 1 to 2 billion to find a number that is the answer.

How languages differ
Binary search is correct only on a sorted list.
Looking for 30 in the unsorted [30, 10, 20] with bisect_left gives 3 (meaning "not there") with no error.
The starting values matter by one, too: if the example started hi at n - 1 instead of n, then searching [10, 20, 30] for 99, which is larger than everything, would give 2 instead of 3.

Practice