C# Big-O Notation

Big-O notation describes how an algorithm’s work grows as the input grows. It does not predict the exact number of milliseconds a C# program will take; it gives you a language for comparing approaches when the data gets larger. This matters because a solution that feels instant for 100 items can become unusable for 1,000,000 items.

Overview: How Big-O Works

Big-O is a way to describe the upper growth rate of an algorithm in terms of n, where n usually means the number of input items. If a loop checks every element in an array of length n, its time complexity is O(n). If a method reads one dictionary entry by key, its average time complexity is O(1). If a binary search cuts the remaining range in half each step, its time complexity is O(log n).

The key idea is growth, not stopwatch timing. Big-O ignores constant factors and smaller terms. A loop that does three simple operations for each item is still O(n), not O(3n). A method that first scans a list and then prints one line is still O(n), because the constant one-line print does not grow with the input.

In C#, complexity is closely connected to collection internals. Arrays and List<T> store elements in contiguous indexed storage, so reading items[i] is O(1). Searching an unsorted array is O(n) because the program may have to compare every element. A Dictionary<TKey,TValue> uses hash codes and buckets, so lookup is average O(1), although poor hash distribution or many collisions can make individual operations slower. A LinkedList<T> can add or remove a known node in O(1), but finding that node is still O(n).

Big-O can describe time complexity or space complexity. Time complexity is about how many operations grow with input size. Space complexity is about how much extra memory grows with input size. For example, counting values in a dictionary may take O(n) time and O(k) extra space, where k is the number of distinct keys. The CLR still has real implementation details such as allocation, garbage collection, cache locality, method calls, bounds checks, and JIT optimizations, but Big-O helps you identify the dominant shape before measuring exact runtime.

Notation Name Typical C# Example
O(1) Constant Read array[0] or average Dictionary lookup.
O(log n) Logarithmic Binary search on sorted data.
O(n) Linear Loop once through a list.
O(n log n) Linearithmic Most general-purpose efficient sorts.
O(n^2) Quadratic Compare every item with every other item.

Syntax

// Big-O is written as O(expression)
O(1)       // work stays about the same as n grows
O(log n)   // work grows by the number of times n can be divided
O(n)       // work grows in direct proportion to n
O(n log n) // common for efficient comparison-based sorting
O(n^2)     // nested work over the same input
  • O means the order, or growth class, of the algorithm.
  • n is the input size, such as the length of an array or list.
  • log n usually means base 2 in algorithm discussions, but the base is ignored by Big-O because it is a constant factor.
  • Constants are dropped, so O(2n) becomes O(n).
  • Only the dominant term remains, so O(n^2 + n) becomes O(n^2).

Examples

Example 1: Constant Time vs Linear Time

using System;

class Program
{
    static void Main()
    {
        int[] numbers = { 4, 8, 15, 16, 23, 42 };

        Console.WriteLine($"First value: {First(numbers)}");
        Console.WriteLine($"Contains 23: {Contains(numbers, 23)}");
        Console.WriteLine($"Contains 99: {Contains(numbers, 99)}");
    }

    static int First(int[] values)
    {
        return values[0];
    }

    static bool Contains(int[] values, int target)
    {
        for (int i = 0; i < values.Length; i++)
        {
            if (values[i] == target)
            {
                return true;
            }
        }

        return false;
    }
}

Output:

First value: 4
Contains 23: True
Contains 99: False

First is O(1) because it performs one indexed read no matter how large the array is. Contains is O(n) because a missing value requires checking every element. Finding 23 stops early in this small example, but Big-O usually describes the worst case or general growth pattern.

Example 2: Nested Loops Create Quadratic Growth

using System;

class Program
{
    static void Main()
    {
        string[] names = { "Ada", "Ben", "Ada", "Chen", "Ben" };
        PrintDuplicatePairs(names);
    }

    static void PrintDuplicatePairs(string[] values)
    {
        for (int i = 0; i < values.Length; i++)
        {
            for (int j = i + 1; j < values.Length; j++)
            {
                if (values[i] == values[j])
                {
                    Console.WriteLine($"Duplicate {values[i]} at {i} and {j}");
                }
            }
        }
    }
}

Output:

Duplicate Ada at 0 and 2
Duplicate Ben at 1 and 4

The outer loop walks through the input, and the inner loop compares the current item with later items. This is O(n^2) because the number of comparisons grows roughly with the square of the input size. Starting j at i + 1 avoids duplicate comparisons, but it does not change the Big-O class.

Example 3: Replacing Repeated Search with a Dictionary

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        string[] names = { "Ada", "Ben", "Ada", "Chen", "Ben" };
        Dictionary<string, int> counts = CountNames(names);

        foreach (KeyValuePair<string, int> entry in counts)
        {
            Console.WriteLine($"{entry.Key}: {entry.Value}");
        }
    }

    static Dictionary<string, int> CountNames(string[] names)
    {
        Dictionary<string, int> counts = new Dictionary<string, int>();

        foreach (string name in names)
        {
            if (counts.ContainsKey(name))
            {
                counts[name]++;
            }
            else
            {
                counts[name] = 1;
            }
        }

        return counts;
    }
}

Output:

Ada: 2
Ben: 2
Chen: 1

This method loops through the input once, so it is O(n) time on average. The dictionary stores one entry per distinct name, so its extra space is O(k), where k is the number of unique names. The average O(1) dictionary update is what prevents this from becoming a nested-loop solution.

Example 4: Logarithmic Growth with Binary Search

using System;

class Program
{
    static void Main()
    {
        int[] sortedScores = { 10, 18, 25, 31, 44, 59, 72, 86, 91 };
        Console.WriteLine($"Index of 72: {BinarySearch(sortedScores, 72)}");
        Console.WriteLine($"Index of 35: {BinarySearch(sortedScores, 35)}");
    }

    static int BinarySearch(int[] values, int target)
    {
        int left = 0;
        int right = values.Length - 1;

        while (left <= right)
        {
            int middle = left + (right - left) / 2;

            if (values[middle] == target)
            {
                return middle;
            }

            if (target < values[middle])
            {
                right = middle - 1;
            }
            else
            {
                left = middle + 1;
            }
        }

        return -1;
    }
}

Output:

Index of 72: 6
Index of 35: -1

Binary search is O(log n) because each comparison removes about half of the remaining possible indexes. This only works because the array is sorted in the same order used by the comparisons. If the data is unsorted, binary search cannot safely discard either half.

How It Works Step by Step

  1. Identify the input size. For a list algorithm, n is often list.Count; for a grid, you may have both rows and columns.
  2. Count the operations that grow with the input. One fixed assignment is constant; a loop over all items grows with n.
  3. Look for nested growth. A loop inside another loop over the same input usually suggests O(n^2).
  4. Drop constants. Two separate loops over the same array are O(n + n), which simplifies to O(n).
  5. Drop smaller terms. A nested loop plus a single loop is O(n^2 + n), which simplifies to O(n^2).
  6. Consider collection operations. List<T>.Contains is linear, while average Dictionary<TKey,TValue>.ContainsKey is constant.
  7. Separate time from memory. Building a lookup table may improve time complexity while using more space.

At runtime, the JIT compiler turns C# into native machine code, and the CLR manages objects, arrays, and garbage collection. Big-O does not model every detail of that execution. A cache-friendly O(n) array loop may beat a pointer-heavy structure for realistic sizes. Still, when input grows by orders of magnitude, the growth class often dominates micro-level details.

Common Mistakes

Mistake 1: Calling a Linear Method Inside a Loop

foreach (int id in ids)
{
    if (allowedIds.Contains(id))
    {
        Console.WriteLine(id);
    }
}

If allowedIds is a List<int>, each Contains call is O(n). Doing that inside a loop over another collection can become O(n * m). A better approach is to build a HashSet<int> for average constant-time membership checks.

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        int[] ids = { 10, 20, 30, 40 };
        HashSet<int> allowedIds = new HashSet<int> { 20, 40, 60 };

        foreach (int id in ids)
        {
            if (allowedIds.Contains(id))
            {
                Console.WriteLine($"Allowed: {id}");
            }
        }
    }
}

Output:

Allowed: 20
Allowed: 40

Mistake 2: Assuming Big-O Gives Exact Runtime

// Both loops are O(n), but they may not take the same time.
for (int i = 0; i < values.Length; i++)
{
    total += values[i];
}

for (int i = 0; i < values.Length; i++)
{
    Console.WriteLine(values[i]);
}

Both loops are linear, but console output is much slower than integer addition. Big-O explains growth, not the exact cost of each operation. Use Big-O to choose a reasonable algorithm, then measure real code when performance matters.

Mistake 3: Ignoring Space Complexity

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        int[] values = { 1, 2, 2, 3, 3, 3 };
        Dictionary<int, int> counts = new Dictionary<int, int>();

        foreach (int value in values)
        {
            counts[value] = counts.TryGetValue(value, out int current) ? current + 1 : 1;
        }

        Console.WriteLine($"Distinct values: {counts.Count}");
    }
}

Output:

Distinct values: 3

This code is efficient in time, but the dictionary uses extra memory. That is often the right tradeoff, but it is still a tradeoff. Big-O analysis should mention both the time improvement and the additional storage.

Best Practices

  • State what n means before analyzing an algorithm.
  • Distinguish average, best, and worst cases when they differ.
  • Use Dictionary<TKey,TValue> or HashSet<T> for repeated key or membership lookups.
  • Use binary search only when the data is already sorted or sorting once is worth the cost.
  • Remember that sorting is commonly O(n log n), so sorting just to search once is often unnecessary.
  • Do not optimize tiny, simple code just because it has a worse Big-O class; readability and real measurements matter.
  • Analyze space complexity when you build arrays, lists, dictionaries, sets, or recursive call stacks.
  • Prefer clear algorithms first, then benchmark if the input size or user experience demands it.

Practice Exercises

  1. Classify these operations: reading array[5], searching List<string> with Contains, and looking up a key in a Dictionary<int, string>.
  2. Write a method that finds the maximum value in an integer array. What is its time complexity and extra space complexity?
  3. Rewrite a duplicate-finding nested-loop solution using a HashSet<string>. Compare the time and space complexity of both versions.

Summary

  • Big-O notation describes how work or memory grows as input size grows.
  • O(1), O(log n), O(n), O(n log n), and O(n^2) are common growth classes in C# algorithms.
  • Arrays and lists provide constant-time indexing, but unsorted search is linear.
  • Dictionaries and hash sets provide average constant-time lookup by using hashing.
  • Nested loops over the same input often create quadratic growth.
  • Big-O is a model for scalability, not a replacement for measuring real C# code.