Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 21

Understanding Big O Notation for Time Complexity

Recommended resources

Hello! Welcome to the third lesson in our module on "Analyzing Algorithm Efficiency."

In our last session, we got our hands dirty by meticulously counting the primitive operations in algorithms. We ended up with precise but complex expressions for an algorithm's work, like 4n - 1 for finding a maximum value, or (n² - n) / 2 for checking for duplicates.

You might have felt that this process was a bit tedious. While it's a crucial skill to understand where the work comes from, in practice, we rarely use these exact formulas. The differences between 4n - 1 and 4n + 5 are insignificant when n is a million. What truly matters is the part of the expression that grows the fastest—the 4n or the n².

Today, we will learn the standard, powerful notation used by programmers and computer scientists to formalize this idea. Your learning outcome is to define Big O notation and use it to express an algorithm's time complexity. This is the language of algorithm analysis and a fundamental concept for technical interviews.


1. The Big Picture: What is Big O?

At its heart, Big O notation is a way to describe how an algorithm's runtime scales as the size of the input grows. It's not about measuring time in seconds; it's about understanding the relationship between the input size and the number of operations.

This first article explains the concept from a practical, interview-oriented perspective.

Big O Notation Time and Space Complexity

The article 'Big O Notation Time and Space Complexity' from Skilled.dev provides an excellent, non-intimidating introduction to why Big O is important for evaluating code.

Please read the first three sections: the introduction, 'How Candidates are Graded for Hiring', and 'What Big O Means'. Focus on the idea that Big O provides an objective way to measure an algorithm's scalability, which is critical when dealing with very large inputs.

The key idea is that we want to understand an algorithm's performance when the input size, n, is very large. As the article highlights, the difference between an efficient and an inefficient algorithm becomes dramatic as n increases.

This is best understood visually.

Big O Notation Time Complexity Graph
This graph illustrates the growth rates of common Big O complexities. The x-axis is the input size (n), and the y-axis is the number of operations. Notice how functions like O(n²) and O(2ⁿ) curve upwards dramatically, while O(n) and O(log n) grow much more slowly. This visualizes why we prefer algorithms with lower complexity.

As you can see, an algorithm with O(n²) complexity becomes unusably slow for large inputs compared to an O(n) algorithm. Big O notation gives us the tool to make these comparisons.



2. The Rules of Simplification

So, how do we get from our exact operation count like 4n - 1 to a clean Big O expression? We follow a few simple rules, which are all aimed at isolating the term that has the biggest impact on growth as n becomes large.

This video from CS Dojo provides a great explanation of the simplification process.

Introduction to Big O Notation and Time Complexity (Data Structures & Algorithms #7)

In this section of his 'Introduction to Big O Notation' video, Y.K. from CS Dojo explains how to simplify complexity expressions to arrive at the final Big O notation.

Watch the clip from 22:12 to 25:22. The video clearly explains the two main rules: dropping constants and dropping non-dominant (lower-order) terms.

Let's formalize these rules, which are also outlined in the "4 Steps to Calculate Big O" section of the Skilled.dev article you read earlier (LINK).

Rule 1: Keep the Fastest-Growing Term (Drop Non-Dominant Terms)

An algorithm's runtime is often a sum of operations, like n² + n + 50. As n gets very large, the n² term will grow so much faster than the n term that the n term's contribution becomes negligible.

  • Example: If n = 1,000,000, then n² is one trillion, while n is "only" one million. The n² term completely dominates.
  • Application: For an operation count of T(n) = n² + n + 50, we drop the n and 50, leaving us with n².

Rule 2: Drop Constant Factors

Big O notation ignores constant multipliers. The reason is that these constants depend more on the programming language or hardware than on the algorithm's fundamental design. An algorithm with 2n operations and one with 10n operations both scale linearly with the input size. If you double the input, the runtime roughly doubles for both.

  • Example: We treat T(n) = 4n and T(n) = 100n as being in the same complexity class.
  • Application: For a count of T(n) = 4n, we drop the constant 4, leaving us with n.

Applying the Rules to Our Previous Examples

Let's revisit the operation counts from our last lesson.

  1. find_max function: We calculated a worst-case count of T(n) = 4n - 1.

    • Apply Rule 1: The fastest-growing term is 4n. We drop the -1. We are left with 4n.
    • Apply Rule 2: We drop the constant 4.
    • Result: The time complexity is O(n) (read as "Big O of n" or "Order n").
  2. has_duplicates function: We calculated a worst-case count of T(n) = (n² - n) / 2, which is 0.5n² - 0.5n.

    • Apply Rule 1: The fastest-growing term is 0.5n². We drop -0.5n. We are left with 0.5n².
    • Apply Rule 2: We drop the constant 0.5.
    • Result: The time complexity is O(n²) (read as "Big O of n-squared" or "Order n-squared").

You can now see how Big O gives us a much cleaner and more direct way to express the core scalability of an algorithm.



3. The Formal Definition of Big O

Given your math background, it's useful to see the formal definition behind this. Big O is a statement about an upper bound.

The following video provides a concise mathematical definition.

1.8.1 Asymptotic Notations Big Oh - Omega - Theta #1

Professor Abdul Bari gives a formal, mathematical definition of Big O notation. This adds a layer of precision to our understanding.

Watch from 02:49 to 03:25. This short segment presents the core definition.

Formally, we say that a function T(n) is O(g(n)) if there exist positive constants c and n₀ such that:

Let's break this down:

  • T(n) is our exact operation count (e.g., 4n - 1).
  • g(n) is the function we use in the Big O notation (e.g., n).
  • The definition says that T(n) is "in the order of" g(n) if, after some input size n₀, our function T(n) will always be less than or equal to g(n) multiplied by some constant c.

In essence, c * g(n) provides an upper bound on our function's growth.

For our T(n) = 4n - 1 example, we can show it is O(n).
Let g(n) = n. Can we find constants c and n₀ such that 4n - 1 \le c \cdot n for all n \ge n₀?
Yes. If we choose c = 4, the inequality becomes 4n - 1 \le 4n, which is true for all n \ge 1. So, we can pick c=4 and n₀=1.

A key point, mentioned in Abdul Bari's video (around timestamp 08:58), is that while 4n - 1 is also technically O(n²) (we could pick c=1 and n₀=4), we always strive to find the tightest possible upper bound. The tightest bound for 4n - 1 is O(n).



4. Practice: Combining Complexities

Let's analyze a function that has multiple parts with different complexities.

def process_data(data):
    n = len(data)




    # Part 1: A simple loop
    print("Printing all items...")
    for item in data:
        print(item)




    # Part 2: A nested loop
    print("Comparing all pairs...")
    for i in range(n):
        for j in range(n):
            print(f"Comparing item {i} with item {j}")

How would we determine the Big O complexity of the entire process_data function?

  1. Analyze Part 1: The first for loop iterates through the list once. This is a classic linear operation. Its complexity is O(n).
  2. Analyze Part 2: The nested for loops execute n * n = n² times. Its complexity is O(n²).
  3. Combine and Simplify: The total work is the sum of the parts: O(n) + O(n²). Now, we apply our simplification rules. The n² term is the fastest-growing (dominant) term.
  4. Final Complexity: We drop the lower-order O(n) term. The overall time complexity of process_data is O(n²).


Conclusion

Congratulations! You've just learned the single most important concept for analyzing algorithm efficiency.

Key Takeaways:

  • Big O notation describes the growth rate of an algorithm's runtime as the input size (n) becomes very large.
  • It provides a high-level, standard way to compare the scalability of different algorithms.
  • To find the Big O complexity, we simplify the exact operation count by:
    1. Focusing on the worst-case scenario.
    2. Dropping constant factors.
    3. Keeping only the fastest-growing (dominant) term.
  • O(n) describes a linear growth rate, while O(n²) describes a quadratic growth rate, which is significantly slower for large inputs.

Preview of the Next Lesson:
Now that you understand what Big O is and the rules for deriving it, the next step is to get faster at recognizing it. In the next lesson, you will learn to identify the time complexity of common patterns: O(1), O(log n), O(n), and O(n²). This will allow you to look at a piece of code and almost instantly classify its efficiency, a critical skill for both problem-solving and technical interviews.