jonnifyelixir
F4 / F4.03 / search

F4.03 · part 2 of 3

Linear & binary search

Linear search checks elements one at a time and works on any sequence — O(n). Binary search checks the middle, throws away the half that cannot contain the target, and repeats — O(log n) — but it only works on data that is sorted and supports jumping to the middle in one step. That second condition is the catch on the BEAM, and it is what sends sorted data into tuples or trees rather than lists.

Searching the sorted array [5, 8, 10, 12, 20, 30, 42] for 20. Select a strategy and watch which elements it touches.

Two ways to find 20

Linear search walks from the left, comparing each element, until it hits 20 at index 4 — five checks. Binary search starts in the middle and halves: 12 is too small, go right; 30 is too big, go left; 20 — found, in three checks. Select a strategy.

The strategy · select one

5 0 8 1 10 2 12 3 20 4 30 5 42 6 check each from the left until 20 — 5 checks

      

cost: O(n)  ·  checks elements one by one — 5 here

result: found 20 at index 4

Both find 20; binary search asks sharper questions. Each comparison it makes discards half of what remains, so it needs about log₂ n of them — three for seven elements, twenty for a million.

Advanced: binary search needs random access

Binary search’s halving assumes you can reach the middle element in one step. On a contiguous structure — a tuple or an :array — elem/2 does that in O(1), so binary search is genuinely O(log n). On a linked list it is a trap: reaching the middle is itself an O(n) walk, so “binary search” over a list is O(n log n) — slower than a plain linear scan. This is why Enum.at/2 on a list is O(n), and why sorted data you intend to search lives in a tuple, not a list.

When you need both fast search and cheap updates, a balanced binary search tree (F4.02) gives O(log n) search without contiguous memory — each step follows one pointer instead of indexing. And when ordering is not required at all, a hash-based map (F4.04) drops the cost to O(1) on average. The structure you choose is the search you can afford.

# linear search — works on any list, O(n)
def linear([x | _], x, i), do: {:ok, i}
def linear([_ | t], x, i), do: linear(t, x, i + 1)
def linear([], _x, _i), do: :error

# binary search — needs O(1) indexing, so a tuple, not a list
def binary(arr, x), do: binary(arr, x, 0, tuple_size(arr) - 1)
defp binary(_arr, _x, lo, hi) when lo > hi, do: :error
defp binary(arr, x, lo, hi) do
  mid = div(lo + hi, 2)
  case elem(arr, mid) do
    ^x -> {:ok, mid}
    v when v < x -> binary(arr, x, mid + 1, hi)
    _ -> binary(arr, x, lo, mid - 1)
  end
end

The idea

Sorted data lets each comparison discard half — but only if you can reach the middle in one step.

In Elixir

Binary search wants a tuple (elem/2 is O(1)); a list forces O(n), a tree gives O(log n).

Next: stability & sort cost — ranking the sorts, and the lower bound none of them can beat.

References

Primary sources for this lesson, and where it connects in the course.

Sources

  • Merge sort — Wikipedia — a stable functional sort.
  • Cormen, Leiserson, Rivest & Stein (2022). Introduction to Algorithms (4th ed.) — sorting and searching, in depth.

Related in this course