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

📚 Interview question

What is Big O notation? Explain O(1), O(log n), O(n), O(n log n), O(n²) and O(2ⁿ) with .NET examples, and the difference between best, average, worst-case and amortised cost.

Big O notation as a growth-rate language: the common complexity classes, real .NET examples of each, and best/average/worst/amortised analysis.

Junior Theory Beginner algorithms big-o complexity fundamentals

🎯 Key points

  • Big O describes how cost grows with input size n, dropping constants and lower-order terms; it is about the shape of growth, not milliseconds
  • Common classes with .NET examples: O(1) Dictionary lookup, O(log n) Array.BinarySearch, O(n) List.Contains, O(n log n) Array.Sort, O(n²) nested loops, O(2ⁿ) naive recursive Fibonacci
  • log n appears whenever each step halves the problem (binary search, balanced tree height, merge sort recursion depth); the log base does not matter
  • Worst, average and best case differ: Dictionary is O(1) average but O(n) worst when keys collide; quicksort is O(n²) worst, which is why .NET uses introsort
  • Amortised cost bounds a sequence of operations: List<T>.Add is amortised O(1) even though a single Add that resizes is O(n)
  • Use separate variables for independent inputs: nested loops over orders and products are O(n · m), not O(n²)
  • Hidden O(n) calls inside loops (Contains, IndexOf, First, Count() on IEnumerable, string +=) quietly create O(n²) code
  • Big O is not the whole story: constants, cache locality, allocations and I/O round trips often dominate at real sizes, so reason with Big O and confirm with benchmarks

⏱ 30-second answer

Big O describes how an algorithm's cost grows as input size n grows, ignoring constants and lower-order terms. It is an upper bound on growth, usually quoted for the worst case.

  • O(1): Dictionary lookup, array index.
  • O(log n): binary search, SortedDictionary lookup.
  • O(n): a single loop, List.Contains.
  • O(n log n): Array.Sort, OrderBy.
  • O(n²): nested loops over the same data.
  • O(2ⁿ): naive recursive Fibonacci, trying every subset.

Amortised O(1) (like List.Add) means occasional expensive steps averaged over many cheap ones.

🗣 Full answer (senior)

Big O is a way to talk about how cost scales, not how fast code is today. If doubling the input doubles the time, it is O(n). If doubling the input quadruples the time, it is O(n²).

The rules

  1. Drop constants: O(3n) is O(n). Constants matter in practice, but Big O is about shape.
  2. Keep the dominant term: O(n² + n) is O(n²).
  3. Different inputs get different letters: looping over orders then customers is O(n + m), nested is O(n · m). Do not call it O(n²) unless both are the same size.

Common classes

Class Name n = 1,000 → 1,000,000 .NET example
O(1) constant same array[i], Dictionary.TryGetValue, HashSet.Contains
O(log n) logarithmic ~10 → ~20 steps Array.BinarySearch, SortedSet.Contains
O(n) linear 1,000× more List.Contains, Sum(), one foreach
O(n log n) linearithmic ~2,000× more Array.Sort, List.Sort, OrderBy
O(n²) quadratic 1,000,000× more nested loops, list.Contains inside a loop
O(2ⁿ) exponential impossible brute-force subsets, naive Fibonacci

Best, average, worst, amortised

  • Worst case: the guarantee. Quicksort is O(n²) worst case, which is why .NET uses introsort to cap it at O(n log n).
  • Average case: expected cost on typical input. Dictionary lookup is O(1) average, O(n) worst if every key collides.
  • Best case: rarely useful ("linear search is O(1) if the item is first").
  • Amortised: the average over a sequence of operations. List<T>.Add is usually O(1) but sometimes copies the whole array when it doubles; across n adds the total is O(n), so each add is amortised O(1).

Production example

An order export took 2 seconds for 1,000 orders in test. In production with 50,000 orders it took over an hour. The code called customers.First(c => c.Id == order.CustomerId) inside a loop over orders: O(n · m). Building a Dictionary<int, Customer> once (O(m)) and looking up inside the loop (O(1)) made it O(n + m) and the export finished in under a second. Big O is how you predict that before production does it for you.

🔍 What the interviewer is checking

Checks that the candidate can classify code by growth rate and connect it to real consequences.

Green flags

  • Explains Big O as growth, drops constants, keeps the dominant term.
  • Gives .NET examples for each class, including hidden O(n) calls like List.Contains.
  • Distinguishes worst, average and amortised (mentions List.Add or Dictionary collisions).
  • Uses n and m for different inputs.
  • Mentions that constants, cache and I/O matter in practice.

Red flags

  • Thinks O(1) means "instant" or Big O measures milliseconds.
  • Cannot explain where log n comes from.
  • Calls everything with two loops O(n²).
  • Has never connected complexity to a real slowdown.

📚 Learn in depth

1. What Big O formally means

f(n) = O(g(n)) means: there exist constants c and n₀ such that for all n ≥ n₀, f(n) ≤ c · g(n).

In plain words, beyond some input size, f never grows faster than a constant multiple of g. The "doubling test" is a practical consequence: if cost is proportional to n², doubling n multiplies the cost by about four once n is large enough for the dominant term to win.

Notation Meaning Analogy
O (Big O) upper bound ≤
Ω (Big Omega) lower bound ≥
Θ (Big Theta) tight bound (both) =

In interviews people say "Big O" when they usually mean the tight bound of the worst case. Saying "binary search is Θ(log n) in the worst case" is the precise version.

2. How the classes grow

Concrete numbers for n = 1,000,000 at roughly 1 ns per step:

Class Steps Time
O(log n) 20 20 ns
O(n) 10⁶ 1 ms
O(n log n) 2 × 10⁷ 20 ms
O(n²) 10¹² ~17 minutes
O(2ⁿ) astronomical never

The difference between O(n log n) and O(n²) at a million rows is milliseconds versus a timeout.

3. Where log n comes from

Any time you halve the problem each step, you get log₂ n steps:

1,000,000 -> 500,000 -> 250,000 -> ... -> 1     (about 20 halvings)

Binary search, balanced tree height and the depth of merge sort's recursion are all log n for this reason. The base of the log does not matter in Big O, because log₂ n and log₁₀ n differ only by a constant factor.

4. Amortised analysis of List<T>.Add

capacity: 4      8              16
          [####] [########]     [################]
           ^ add 5th item: allocate 8, copy 4
                  ^ add 9th: allocate 16, copy 8

Total copies after n adds ≈ 4 + 8 + 16 + ... + n < 2n. So n adds cost O(n) in total, which is amortised O(1) per add. Individual adds can still be O(n), which matters for latency-sensitive code: pre-size with new List<T>(capacity) when you know the count.

5. Average versus worst case in practice

Algorithm Average Worst What triggers the worst case
Dictionary lookup O(1) O(n) Poor GetHashCode or hash flooding
Quicksort (plain) O(n log n) O(n²) Bad pivots on sorted or adversarial input (introsort caps it)
Insertion sort O(n²) O(n²) Reverse order; but O(n) on nearly sorted data
Unbalanced BST O(log n) O(n) Inserting sorted keys

6. Hidden complexity in .NET APIs

Looks like Actually
list.Contains(x) O(n) scan
list.Remove(x) / list.Insert(0, x) O(n) shift
list.RemoveAt(0) in a loop O(n²) total
string += in a loop O(n²) copying; use StringBuilder
enumerable.Count() on a non-collection O(n) enumeration
ElementAt(i) on IEnumerable<T> O(i)
OrderBy(...).First() Sorts or partially sorts; use MinBy for a single O(n) pass
ImmutableList<T> index O(log n), it is a tree

7. Big O is not the whole story

  • Constants and cache effects: an O(n) scan of a small contiguous array often beats an O(log n) tree walk that chases pointers. For n below a few dozen, linear search in an array is frequently fastest.
  • I/O dominates: one database round trip (about 1 ms) costs more than millions of in-memory operations. An O(1) algorithm that does N+1 queries loses to an O(n log n) one that does one query.
  • Allocation: an O(n) LINQ pipeline that allocates per element may be slower than an O(n) loop that does not.
  • Space: time and memory complexity are separate; a hash set makes duplicate detection O(n) time but costs O(n) extra memory.

8. Try it yourself

// dotnet new console; paste into Program.cs; dotnet run -c Release
using System.Diagnostics;

foreach (var n in new[] { 1_000, 10_000, 50_000 })
{
    var list = Enumerable.Range(0, n).ToList();
    var set = list.ToHashSet();

    var sw = Stopwatch.StartNew();
    int hits = 0;
    foreach (var x in list) if (list.Contains(x)) hits++; // O(n²)
    var quadratic = sw.Elapsed;

    sw.Restart();
    foreach (var x in list) if (set.Contains(x)) hits++;  // O(n)
    Console.WriteLine($"n={n,6}: List {quadratic.TotalMilliseconds,8:F1} ms, HashSet {sw.Elapsed.TotalMilliseconds,6:F2} ms");
}

Watch the List column grow about 100× when n grows 10×, while the HashSet column grows about 10×.

💻 Code

```csharp
// O(1): constant, independent of n
int First(int[] a) =&gt; a[0];

// O(log n): halve the search space each step
int idx = Array.BinarySearch(sortedIds, 42);

// O(n): one pass
long Sum(int[] a)
{
long s = 0;
foreach (var x in a) s += x;
return s;
}

// O(n log n): comparison sort
Array.Sort(prices);

// O(n²): nested loop over the same data
bool HasDuplicateSlow(int[] a)
{
for (int i = 0; i &lt; a.Length; i++)
for (int j = i + 1; j &lt; a.Length; j++)
if (a[i] == a[j]) return true;
return false;
}

// O(n): same answer with a HashSet (expected O(1) per Add)
bool HasDuplicateFast(int[] a)
{
var seen = new HashSet

⚠ Common mistakes

  • Mistake: Saying Big O measures speed ("O(1) is fast"). Why it is wrong: Big O measures growth. An O(1) hash lookup that computes an expensive hash can be slower than an O(n) scan of five items. Instead: Say "the cost does not grow with n" and measure real speed with BenchmarkDotNet.

  • Mistake: Calling two nested loops over different collections O(n²). Why it is wrong: If orders has n items and products has m, the cost is O(n · m). With 1,000,000 orders and 10 products it behaves like O(n). Instead: Use a separate letter for each independent input.

  • Mistake: Ignoring hidden loops such as List.Contains, IndexOf, Any, First or string += inside a loop. Why it is wrong: Each one is O(n), so the outer loop becomes O(n²) without any visible nested for. Instead: Read the cost of every call in a hot loop, and build a Dictionary or HashSet once outside it.

  • Mistake: Quoting only the average case, for example "Dictionary is always O(1)". Why it is wrong: With a bad GetHashCode or a hash-flooding attack every key collides and lookups become O(n). Instead: State average and worst case, and what makes the worst case happen.

  • Mistake: Confusing amortised with average. Why it is wrong: Average is over possible inputs; amortised is a guaranteed bound over a sequence of operations. List.Add is amortised O(1) but a single add can be O(n). Instead: Use "amortised" for sequences, and pre-size collections in latency-sensitive paths.

  • Mistake: Optimising Big O while ignoring I/O. Why it is wrong: One extra database or HTTP call per item dwarfs CPU work. Instead: Count round trips first, then in-memory complexity.

🏛 Senior insight: trade-offs

Seniors use Big O as a prediction tool for scale, not an exam topic. Before shipping, they ask "what is n in production, and how will it grow in two years?" O(n²) on 50 config entries is fine forever; O(n²) on customer orders is a future incident.

They also know when Big O misleads: for small n, contiguous arrays and linear scans win because of CPU caches; for I/O-bound systems, round trips dominate everything. The senior move is: reason with Big O, confirm with a benchmark at realistic n, and watch production metrics (p99 latency versus input size) for curves that bend upward.

Finally, they look for complexity in the library calls, not just the loops they wrote: Contains, Count(), ElementAt, OrderBy().First(), immutable collections and EF queries that materialise too early.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions hidden O(n) calls inside loops Why: Tests whether they can apply Big O to real code with LINQ and string building
    How to answer

    Count the work per line, including hidden loops in library calls. With O orders, L lines per order, P products:

    • products.FirstOrDefault per line: O(O · L · P).
    • seen.Contains on a List: O(O²).
    • report += copies the whole string each time: O(total chars²).

    Fix: build Dictionary<int, Product> once (O(P)), use HashSet<int> for seen, and use StringBuilder. Total becomes O(P + O · L + output size), linear in the input.

  2. When: Candidate only talks about time Why: Checks they also reason about memory growth, stack depth and GC
    How to answer

    Space complexity is how the extra memory an algorithm needs grows with input size. Usually we quote auxiliary space, not counting the input.

    • Reversing an array in place: O(1).
    • Copying to a new list, or a HashSet of seen items: O(n).
    • Recursion uses stack: depth d costs O(d) (merge sort O(log n) stack plus O(n) buffer).

    In .NET, space also means allocations and GC pressure: an O(n) algorithm that allocates per item can be slower than one that reuses a buffer. Common trade: spend O(n) memory (a dictionary, memo table) to save time.

  3. When: Candidate says log n comes from halving Why: Checks they can implement the classic O(log n) algorithm correctly
    How to 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.

↩ Usually asked after

More in Algorithms

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me