Skip to main content

Welcome to a Grasp lesson

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

Lesson 1

Big-O Analysis of Iterative Algorithms

Recommended resources

Introduction

Welcome to your first lesson in the "Data Structures and Algorithms with C++" course! Your goal is to prepare for coding interviews, and a rock-solid understanding of algorithmic complexity is the absolute foundation for that. Today, we'll focus on a critical first step: learning how to determine the time complexity of iterative algorithms using Big-O notation.

While you might have a general awareness of Big-O, this lesson will formalize that knowledge. We will break down how to analyze C++ code—specifically loops—to predict its performance as the input size grows. This skill is non-negotiable in an interview setting, as it allows you to discuss the trade-offs between different solutions and justify why one is better than another.


What is Time Complexity and Why Does It Matter?

Imagine you've written a search function for an e-commerce site. It works perfectly with 100 products. But when the inventory grows to 100,000 products, the search slows to a crawl, and users start to complain. The code is correct, but it doesn't scale. This is the core problem that time complexity analysis helps us solve before we run into trouble.

Big-O notation is the language we use to talk about this. It doesn't measure the exact time in seconds an algorithm takes to run—that depends on the hardware, the programming language, and many other factors. Instead, it describes the growth rate of the runtime as the input size, which we'll call n, gets larger. It specifically describes the upper bound or the worst-case scenario.

To get a feel for what this means, I recommend starting with this short segment from a video by NeetCode, a popular resource for interview preparation.

Big-O Notation - For Coding Interviews

Watch this first part of the video "Big-O Notation - For Coding Interviews".

Focus on the explanation of what Big-O is and the key idea that we ignore constant factors and less significant terms. Pay attention to the initial graphs showing different growth rates from the beginning.

The key takeaway is that we're interested in the "order of" growth. An algorithm with a complexity of and one with are both considered to have complexity because they both grow linearly. As n becomes very large, the dominant term in the complexity function is the only one that matters.

This graph visually demonstrates why this is so important.

This graph illustrates how dramatically runtime can differ as input size increases. An algorithm with quadratic complexity, O(n²), becomes unusable much faster than one with linear complexity, O(n).

As you can see, an algorithm with complexity becomes drastically slower than an or algorithm as the input size n grows. In a coding interview, choosing an algorithm from the "better" side of this chart is often the main challenge.


Analyzing Basic Iterative Structures

Let's start analyzing some real C++ code. Since you're comfortable with loops, we'll begin there. Most time complexity analysis for iterative algorithms boils down to counting how many times the statements inside a loop are executed as a function of the input size n.

O(1) — Constant Time

An algorithm has constant time complexity if its execution time does not change regardless of the input size. These are the most efficient operations.

// Returns the first element of a vector.
// The size of the vector does not affect how long this takes.
int getFirstElement(const std::vector<int>& data) {
    if (data.empty()) {
        throw std::runtime_error("Vector is empty");
    }
    return data[0]; // This is an O(1) operation
}

Accessing an element in a std::vector by its index is an operation. The computer can calculate the memory address of data[0] (or any data[i]) directly, so it doesn't matter if the vector has 10 elements or 10 million.

O(n) — Linear Time

This is one of the most common complexities. The runtime grows in direct proportion to the input size n. If the input doubles, the runtime roughly doubles. This typically occurs when we need to iterate through all elements of a collection once.

// Finds the maximum value in a vector.
int findMaximum(const std::vector<int>& data) {
    if (data.empty()) {
        throw std::runtime_error("Vector is empty");
    }
    int maximum = data[0]; // O(1)
    // The loop runs 'n' times, where n is data.size()
    for (int value : data) { // O(n)
        if (value > maximum) { // O(1) inside the loop
            maximum = value;
        }
    }
    return maximum;
}

In this example, the for loop runs data.size() times. If we call data.size() as n, the total time is roughly proportional to n. The work inside the loop is constant time, . Therefore, the total complexity is , which is .

This diagram shows how a simple loop that iterates `n` times, with each iteration taking constant time O(1), results in an overall time complexity of O(n).

A common point of confusion is what happens if the loop doesn't increment by one. The following video by Abdul Bari clarifies this point well.

1.5.1 Time Complexity #1

In his video "Time Complexity #1", Professor Bari explains the complexity of basic loops.

Watch the segment from 1:29 to 2:10. Notice how even if a loop increments by 2, or 20, the complexity remains O(n). This is because Big-O notation ignores constant factors (n/2 or n/20 is still fundamentally linear).

What if you have two separate, non-nested loops?

void processTwoVectors(const std::vector<int>& vec1, const std::vector<int>& vec2) {
    int n = vec1.size();
    int m = vec2.size();

    // First loop runs n times
    for (int i = 0; i < n; ++i) {
        // O(1) work
    }

    // Second loop runs m times
    for (int i = 0; i < m; ++i) {
        // O(1) work
    }
}

In this case, the loops are sequential, not nested. We add their complexities. The total time complexity is . We don't simplify this to unless we know that m is always proportional to n, because n and m could be independent and vary wildly.


Analyzing Nested Loops

Things get more interesting when we nest loops. This is where complexities like often appear.

O(n²) — Quadratic Time

A common pattern for quadratic complexity is a loop nested inside another loop, where both iterate up to n.

// Checks if a vector contains duplicate values.
bool hasDuplicate(const std::vector<int>& data) {
    int n = data.size();
    for (int i = 0; i < n; ++i) {       // Outer loop runs n times
        for (int j = i + 1; j < n; ++j) { // Inner loop runs... how many times?
            if (data[i] == data[j]) {
                return true;
            }
        }
    }
    return false;
}

Notice the inner loop starts from j = i + 1. This means the number of its iterations depends on the outer loop's variable, i.

  • When i = 0, the inner loop runs n-1 times.
  • When i = 1, the inner loop runs n-2 times.
  • ...
  • When i = n-2, the inner loop runs 1 time.

The total number of operations is the sum . This is a well-known arithmetic series that sums to , which expands to .

Following the rules of Big-O, we drop the constant factor () and the non-dominant term (), leaving us with .

To really solidify this concept, let's look at two different but complementary explanations.

  1. The Formal Derivation: Abdul Bari provides a clear, step-by-step trace and derivation.

1.5.1 Time Complexity #1

Watch this segment from the same "Time Complexity #1" video.

The analysis from 2:50 to 5:37 shows exactly how to trace the loop variables and arrive at the sum that leads to O(n^2).

  1. The Intuitive Visualization: NeetCode offers a more visual, intuitive way to think about this sum.

Big-O Notation - For Coding Interviews

Now watch this part of the "Big-O Notation - For Coding Interviews" video.

From 7:57 to 9:09, the video explains why this pattern results in roughly n*n/2 operations, visualizing it as half of a square. This is a great mental shortcut for interviews.


A Look at Other Common Iterative Complexities

Not all loops are linear or quadratic. Let's explore two other important patterns that can arise in iterative algorithms.

O(log n) — Logarithmic Time

Logarithmic complexity is a hallmark of highly efficient algorithms. It typically appears when the algorithm halves the size of the input it has to consider in each iteration. The classic example is binary search on a sorted array.

Understanding Big O Notation: A Beginner's Guide to Algorithm ...

This article provides an excellent C++ implementation and explanation for iterative binary search.

In the section on common complexities, find the subsection for O(log n). Read the explanation and study the binarySearch C++ code. The key insight is that with each comparison, you eliminate half of the remaining elements.

The number of times you can divide n by 2 until you get to 1 is given by . In Big-O notation, the base of the logarithm is considered a constant factor, so we simply write .

O(√n) — Square Root Time

Sometimes, the loop control variable changes in a non-linear way, leading to less obvious complexities. Consider this code:

int i = 0;
int p = 0;
while (p <= n) {
    i++;
    p = p + i;
}

Here, the while loop continues as long as p is less than or equal to n. But p doesn't grow linearly; it grows by i, which itself is incrementing.

  • After 1 iteration: p = 1
  • After 2 iterations: p = 1 + 2 = 3
  • After 3 iterations: p = 1 + 2 + 3 = 6
  • After k iterations: p = 1 + 2 + ... + k = k(k+1)/2

The loop stops when p > n. So, we're looking for the value of k where . This simplifies to , or . Dropping the constant, the number of iterations k is proportional to , making the time complexity .

1.5.1 Time Complexity #1

Professor Bari explains this exact pattern in his video.

Watch his analysis of this code from 5:55 to 8:24 to see the derivation in action. This is a great example of a tricky loop that might come up in an interview.


A Practical Summary for Interviews

When you're in an interview, you need a quick way to analyze code. Here's a practical guide based on what we've covered.

Understanding Big O Notation: A Beginner's Guide to Algorithm ...

The section "Analyzing Your Own Code" provides a great checklist.

Please read the entire section, starting from this point. It summarizes the key rules: Identify n: What represents the input size? Count Operations: Focus on loops and how many times they run relative to n. Combine Complexities: Add for sequential loops (O(n) + O(n) = O(n)), and multiply for nested loops (O(n) \times O(n) = O(n^2)). Simplify: Drop constant factors and keep only the fastest-growing (dominant) term.

In a typical 45-minute coding interview, you'll be given a problem with input constraints. For example, n might be up to 10^5. A modern computer can perform roughly 10^8 operations per second.

  • An solution would require operations, which is far too slow.
  • An solution would require roughly , which is about operations. This is perfectly acceptable.
  • An solution would be even better.

This practical estimation is crucial for guiding you toward an optimal solution.


Conclusion

In this lesson, we established the importance of time complexity and Big-O notation as tools for evaluating algorithms, a critical skill for coding interviews. You've learned how to analyze iterative C++ code and determine its time complexity by examining its loop structures.

Key Takeaways:

  • Big-O notation describes the growth rate of an algorithm's runtime in the worst case as the input size n increases.
  • We simplify complexity by dropping constants and non-dominant terms.
  • A single loop iterating through n elements is typically .
  • Sequential loops have their complexities added ().
  • Nested loops have their complexities multiplied (often leading to ).
  • Algorithms that reduce the problem size by a constant factor (e.g., halving it) in each step are often .
  • Complex loop structures can lead to other complexities like .

We've focused exclusively on time complexity—how long an algorithm takes. However, performance isn't just about speed; it's also about memory. In the next lesson, we will explore space complexity, which measures how an algorithm's memory usage scales with its input size.