📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

How does binary search work? Implement it in C#, explain its preconditions and edge cases, and compare it with linear search and hash lookup. When would you use each in a .NET application?

Searching algorithms: linear search, binary search (with a correct implementation and edge cases), hash lookup, and choosing between them in .NET.

Intermediate Coding Intermediate algorithms big-o binary-search collections searching

🎯 Key points

  • Linear search is O(n), needs no ordering, and .NET vectorises IndexOf and Contains for primitives, so it is often fastest for small arrays or one-off searches
  • Binary search is O(log n) and requires data sorted by the same comparer used for the search; a million items need about 20 comparisons
  • Pick one interval convention and keep it: closed [lo, hi] with while (lo <= hi), or half-open [lo, hi) with while (lo < hi); mixing them causes off-by-one errors and infinite loops
  • Compute mid as lo + (hi - lo) / 2 so lo + hi cannot overflow int
  • Array.BinarySearch returns the index when found, otherwise the bitwise complement (~) of the insertion point, which gives sorted inserts and nearest-value lookups for free
  • With duplicates it returns any matching index; lower-bound (first ≥ x) and upper-bound (first > x) variants answer first-occurrence, count and range questions
  • Hash lookup is O(1) average but answers only exact matches and costs O(n) time and memory to build; it cannot answer 'nearest at or below' or range queries
  • Binary search applies to any monotonic predicate, not just arrays: git bisect, the smallest batch size that meets a deadline, the first failing configuration value

⏱ 30-second answer

  • Linear search: check each item, O(n), works on anything, fastest for tiny or unsorted data. IndexOf, Contains, First.
  • Binary search: needs sorted data; compare with the middle and discard half each step, O(log n). Array.BinarySearch, List<T>.BinarySearch, span BinarySearch. Returns the bitwise complement of the insertion point when not found.
  • Hash lookup: O(1) average, needs a HashSet/Dictionary built first, only for exact matches.

Use binary search for sorted data, range queries ("first item ≥ x") and when you cannot afford a hash index.

🗣 Full answer (senior)

Linear search

Check every element until you find it. O(n) time, O(1) space, no preconditions. .NET's Array.IndexOf, Span.IndexOf and string.IndexOf are vectorised with SIMD, so for small or medium arrays of primitives linear search is very fast.

Binary search

Requires the data to be sorted by the same comparison you search with. Each step compares the target with the middle element and discards half:

search 23 in [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
 lo=0 hi=9 mid=4 -> 16 < 23, go right
 lo=5 hi=9 mid=7 -> 56 > 23, go left
 lo=5 hi=6 mid=5 -> 23 found  (3 comparisons instead of 6)

O(log n) time: a million items need at most about 20 comparisons.

Hash lookup

HashSet<T>.Contains / Dictionary.TryGetValue: O(1) average. Costs O(n) to build and extra memory, and only answers exact match questions. It cannot answer "the nearest price below 50" or "all timestamps between 10:00 and 10:05".

Choosing

Need Best choice
Tiny collection (< ~32 items) or searched once Linear (Contains, IndexOf)
Many exact lookups HashSet / Dictionary / FrozenDictionary
Sorted data, range or nearest queries Binary search, SortedSet.GetViewBetween
Data already sorted in a file or array, no memory for an index Binary search
Searching for any of a set of chars/bytes/strings SearchValues<T> with IndexOfAny
Database an index (B+tree), which is binary-search-like on disk

Production example

A rate-card service looked up tariffs by effective date with tariffs.Where(t => t.From <= date).OrderByDescending(t => t.From).First(): O(n log n) per request. Tariffs change rarely, so we sorted them once into an array and used a binary search for the last tariff with From <= date, which made each lookup O(log n) with no allocations. A dictionary could not do this because the query is "nearest at or below", not an exact key.

🔍 What the interviewer is checking

Checks the candidate can implement binary search correctly and choose the right search for the job.

Green flags

  • Writes a correct loop with consistent bounds and overflow-safe mid.
  • States the sorted precondition and the same-comparer rule.
  • Knows Array.BinarySearch returns ~insertionPoint.
  • Compares with hash lookup (exact only) and linear search (small n, vectorised).
  • Mentions lower bound or range queries.

Red flags

  • Infinite loops or off-by-one errors in the implementation.
  • Thinks binary search works on unsorted data.
  • Always picks a Dictionary, even for range queries.
  • Cannot explain why it is O(log n).

📚 Learn in depth

1. A correct binary search

Binary search is famous for off-by-one bugs. Use a half-open or closed interval consistently.

// Closed interval [lo, hi]. Returns index, or ~insertionPoint when not found (same contract as Array.BinarySearch).
static int BinarySearch(ReadOnlySpan<int> a, int target)
{
    int lo = 0, hi = a.Length - 1;
    while (lo <= hi)
    {
        int mid = lo + ((hi - lo) >> 1);   // avoids int overflow of (lo + hi)
        if (a[mid] == target) return mid;
        if (a[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return ~lo;                            // lo is where target would be inserted
}

Why lo + (hi - lo) / 2? With arrays near int.MaxValue elements, lo + hi can overflow to a negative number. This exact bug sat in the JDK's binary search for about nine years.

2. The insertion-point contract

Array.BinarySearch returns a negative number when the item is missing: the bitwise complement of the index where it would go.

int[] a = { 10, 20, 30 };
int i = Array.BinarySearch(a, 25);   // returns ~2 == -3
int insertAt = ~i;                   // 2

This makes "insert in sorted order" and "find nearest" cheap.

3. Lower bound and upper bound

Most real problems are not "is x here?" but "where does x start?":

index:   0  1  2  3  4  5
value:  [1, 3, 3, 3, 7, 9]
lowerBound(3) = 1   (first >= 3)
upperBound(3) = 4   (first > 3)
count of 3s = upper - lower = 3
static int LowerBound(ReadOnlySpan<int> a, int x)
{
    int lo = 0, hi = a.Length;           // half-open [lo, hi)
    while (lo < hi)
    {
        int mid = lo + ((hi - lo) >> 1);
        if (a[mid] < x) lo = mid + 1; else hi = mid;
    }
    return lo;
}

Array.BinarySearch returns some matching index when duplicates exist, not necessarily the first. Use a lower bound when you need the first.

4. Binary search on the answer

Binary search works on any monotonic predicate, not just arrays. "What is the smallest batch size that finishes within the time limit?" If ok(size) is false then true as size grows, binary-search the size. The same trick finds the first bad build in a range (that is what git bisect does).

5. Preconditions that bite in .NET

Pitfall Result
Data not sorted wrong answers, no exception
Sorted with one comparer, searched with another (e.g. OrdinalIgnoreCase vs default culture) misses items
Sorting strings with culture rules, then searching ordinally misses items
Mutating keys after sorting silently wrong

Always pass the same comparer to Sort and BinarySearch.

6. Linear search is not always slow

Modern CPUs prefetch sequential memory and .NET vectorises IndexOf for primitives with Vector128/256/512. Binary search jumps around memory and mispredicts branches. For arrays smaller than roughly 32–64 ints, a linear scan often wins. Measure before "optimising" a tiny list.

7. Try it yourself

var data = Enumerable.Range(0, 1_000_000).Select(i => i * 2).ToArray(); // sorted even numbers
Console.WriteLine(Array.BinarySearch(data, 500_000));   // 250000
int miss = Array.BinarySearch(data, 7);
Console.WriteLine($"{miss} -> insert at {~miss}");     // -5 -> insert at 4
Console.WriteLine(LowerBound(data, 7));                  // 4

static int LowerBound(ReadOnlySpan<int> a, int x)
{
    int lo = 0, hi = a.Length;
    while (lo < hi) { int mid = lo + ((hi - lo) >> 1); if (a[mid] < x) lo = mid + 1; else hi = mid; }
    return lo;
}

Then break it on purpose: shuffle data and see BinarySearch return nonsense without throwing.

💻 Code

// Effective-dated lookup: last tariff whose From <= date
public sealed class TariffTable
{
    private readonly Tariff[] _byFrom;      // sorted once
    private readonly DateOnly[] _from;

    public TariffTable(IEnumerable<Tariff> tariffs)
    {
        _byFrom = tariffs.OrderBy(t => t.From).ToArray();
        _from = Array.ConvertAll(_byFrom, t => t.From);
    }

    public Tariff? For(DateOnly date)
    {
        int i = Array.BinarySearch(_from, date);
        if (i < 0) i = ~i - 1;              // insertion point minus one = last From <= date
        return i >= 0 ? _byFrom[i] : null;
    }
}

// Exact lookups: build once, O(1) each
var blocked = new[] { "[email protected]", "[email protected]" }.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
bool isBlocked = blocked.Contains(email);

// Searching for any of several characters: SearchValues (vectorised)
private static readonly SearchValues<char> Separators = SearchValues.Create(",;|\t");
int pos = line.AsSpan().IndexOfAny(Separators);

// Same comparer for sort and search
var names = new List<string> { "bob", "Alice", "carol" };
names.Sort(StringComparer.OrdinalIgnoreCase);
int idx = names.BinarySearch("ALICE", StringComparer.OrdinalIgnoreCase); // 0

⚠ Common mistakes

  • Mistake: Binary searching unsorted data. Why it is wrong: The algorithm discards half the range based on order; on unsorted data it returns wrong results without throwing. Instead: Sort once (or keep data in a sorted structure) and assert sortedness in tests.

  • Mistake: Mixing interval conventions, for example hi = a.Length with while (lo <= hi). Why it is wrong: The loop reads past the end or never terminates when hi = mid meets a closed interval. Instead: Use closed [lo, hi] with hi = mid - 1, or half-open [lo, hi) with hi = mid, and test empty, one-element, first, last and missing cases.

  • Mistake: Computing mid = (lo + hi) / 2. Why it is wrong: For very large indexes lo + hi overflows int and becomes negative. Instead: Use lo + (hi - lo) / 2.

  • Mistake: Treating a negative BinarySearch result as just "not found". Why it is wrong: It throws away the insertion point, which is exactly what nearest-value and sorted-insert need. Instead: Use ~result to get the insertion index.

  • Mistake: Sorting with one comparer and searching with another. Why it is wrong: Culture-sensitive and ordinal orderings differ, so the search walks the wrong way and misses items. Instead: Pass the same IComparer<T> or StringComparer to both.

  • Mistake: Assuming the result is the first duplicate. Why it is wrong: Array.BinarySearch returns any matching index. Instead: Write a lower-bound search when you need the first occurrence or a count.

  • Mistake: Replacing every Contains with a hash set or binary search. Why it is wrong: For a handful of items a vectorised linear scan is faster, and building the index costs more than it saves. Instead: Pick by size and number of lookups, and measure.

🏛 Senior insight: trade-offs

Seniors pick the search by the question being asked: exact match → hash; ordered, range or nearest → sorted structure plus binary search; one-off or tiny → linear. Databases make the same choice: a B+ tree index supports WHERE CreatedAt BETWEEN ..., while a hash index cannot.

Situation Choice Why, beyond Big O
Under ~32–64 primitives, or searched once Linear (IndexOf, Contains) Vectorised, sequential memory, no build cost
Thousands of exact lookups Dictionary, HashSet, FrozenDictionary O(1) average, build cost amortised
Sorted data with range, nearest or insert-position queries Binary search, SortedSet.GetViewBetween Exact-match structures cannot answer these
Huge sorted file or memory-mapped data Binary search in place No extra memory for an index
Searching for any of several characters SearchValues<T> with IndexOfAny SIMD across the whole set

They respect constants: binary search jumps around memory and its branches are hard to predict, so for small arrays a linear scan can win even though it is O(n). They also treat binary search as a general technique for monotonic predicates (capacity planning, git bisect). The senior answer names the crossover point and says "we measured".

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says binary search needs sorted data Why: Checks they know how .NET sorts and the cost of sorting first
    How to answer

    Array.Sort, List<T>.Sort and Span.Sort use introsort: quicksort, switching to heapsort if recursion gets too deep (guarantees O(n log n)) and insertion sort for small partitions (16 items or fewer). It is in place and not stable.

    Enumerable.OrderBy / ThenBy (and Order) are stable and allocate a copy.

    Comparison sorts cannot beat O(n log n). For small integer keys, counting or radix sort is O(n + k). Most of the time the real choice is: sort in the database, sort in place, or use a stable LINQ sort.

  2. When: Candidate mentions sorted structures or range queries Why: Connects binary search to BSTs and B+tree indexes
    How to answer

    A tree is a hierarchy of nodes with one root, where each node has children and no cycles. A binary tree limits each node to two children (left, right). A binary search tree (BST) adds an ordering rule: left subtree < node < right subtree, so search, insert and delete take O(height).

    If the tree is balanced, height is O(log n); if you insert sorted data into a plain BST it degenerates into a linked list with O(n) height. Self-balancing trees (red-black, AVL) keep O(log n); .NET's SortedSet and SortedDictionary are red-black trees.

    Traversals: in-order (sorted output for a BST), pre-order (copy/serialise), post-order (delete/evaluate), level-order (BFS).

    Trees are everywhere: database B+ tree indexes, file systems, JSON/XML/DOM, Roslyn syntax trees, expression trees, org charts.

  3. When: Candidate compares with hash lookup Why: Checks hash table internals and worst cases
    How to answer

    A hash table turns a key into an integer hash, maps it to a bucket (hash % bucketCount), and stores the entry there. Lookups compute the same hash, go straight to the bucket, and compare keys with Equals. Average cost is O(1); worst case (everything in one bucket) is O(n).

    .NET's Dictionary<TKey,TValue> keeps an int[] buckets and an Entry[] entries array; colliding entries form a chain via a next index inside the entries array. When full, it resizes to roughly double (a prime size) and rehashes.

    Contract: if a.Equals(b) then a.GetHashCode() == b.GetHashCode(), and a key's hash must not change while it is in the dictionary. Good hashes spread keys evenly.

↩ Usually asked after

More in Algorithms

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me