Skip to main content

Welcome to a Grasp lesson

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

Lesson 2

Analyzing Loop Structures for Big-O Time Complexity

Recommended resources

Hello! In our last lesson, we focused on "performance budgeting"—using a problem's constraints to determine the required time complexity for your algorithm. This is your strategic first step in any coding interview.

Today, we'll learn the other side of the coin: how to analyze an iterative algorithm you've written to determine its actual time complexity. By mastering this, you can verify whether your solution meets the performance budget you identified. Given your background in architecture, you can think of this as moving from high-level system performance estimation to analyzing the performance characteristics of a single, self-contained service or component—in this case, your algorithm.

This lesson directly addresses the learning outcome: Derive Big-O time complexity for iterative algorithms by analyzing loop structures.

1. The "First Principles" of Big-O Notation

Before we start counting loops, let's quickly revisit the core idea of Big-O notation. It's a mathematical way to describe how an algorithm's runtime or space requirements grow as the input size (N) gets very large. We are not concerned with the exact number of operations, but with the rate of growth.

The formal definition states that a function T(N) (the exact operation count) is O(F(N)) if T(N) is less than or equal to some constant multiple of F(N) for all sufficiently large values of N.

Big O Analysis Graph
This graph shows that for an input size `N` greater than some point `n0`, the actual runtime `f(n)` is always bounded above by `c*g(n)`. This means `f(n)` grows no faster than `g(n)`.

This formal definition leads to two practical rules for simplification:

  1. Drop Constants: An algorithm that takes 2N steps and one that takes 100N steps are both considered O(N). The growth rate is linear in both cases.
  2. Drop Lower-Order Terms: For large N, the term with the highest power dominates. An algorithm taking N² + 50N + 100 steps is simplified to O(N²), because as N becomes very large, the N² term's contribution dwarfs the others.

To see these principles explained with code, the following video provides a clear walkthrough.

Big O Notation & Time Complexity Analysis Tutorial

The video 'Big O Notation & Time Complexity Analysis Tutorial' from Tech With Tim offers a great explanation of these simplification rules.

Please watch from 10:57 to 21:29. This segment demonstrates why we can ignore lower-order terms and constants when determining Big-O complexity for both additive and multiplicative expressions. This is the foundational logic for the analysis we'll be doing.


2. A Systematic Approach to Analyzing Loops

Now, let's apply these principles to analyze code. The structure of the loops in an algorithm is the primary determinant of its time complexity.

Rule 1: Sequential Statements and Loops (Addition)

When you have statements or loops that execute one after another (sequentially), you add their complexities. The final Big-O is determined by the dominant term.

Consider this structure:

// Part 1: O(N)
for (int i = 0; i < N; i++) {
    // constant time work
}

// Part 2: O(M)
for (int j = 0; j < M; j++) {
    // constant time work
}

The total time complexity is O(N) + O(M) = O(N + M).

  • If M is significantly smaller than N, this can be simplified to O(N).
  • If they are independent inputs, you must keep both: O(N + M).

This is explained well in the following resource.

Complexity and Big-O Notation

The article 'Complexity and Big-O Notation' from the University of Wisconsin-Madison provides clear, structured rules for complexity analysis.

Read the part of the 'How to Determine Complexities' section that covers 'Sequence of statements' and the first example in 'TEST YOURSELF #3'. This will solidify the concept of adding complexities for sequential blocks.

Rule 2: Nested Loops (Multiplication)

When one loop is nested inside another, you multiply their complexities.

a) Independent Nested Loops
The inner loop's iterations do not depend on the outer loop's variable.

// Outer loop runs N times
for (int i = 0; i < N; i++) {
    // Inner loop runs M times for each outer iteration
    for (int j = 0; j < M; j++) {
        // constant time work
    }
}

The total number of operations for the inner body is N * M. The complexity is O(N * M). If M is equal to N, this becomes the very common O(N²).

The following short video breaks this down clearly.

Time Complexity of Nested Loops (Independent Loops)

The video 'Time Complexity of Nested Loops (Independent Loops)' from Neso Academy explains precisely why we multiply complexities for independent nested loops.

Watch from 00:49 to 13:37. The video first explains the concept from first principles by counting operations, and then introduces the simpler multiplication shortcut. This aligns with your preference for understanding the 'why'.

b) Dependent Nested Loops
This is a frequent pattern where the inner loop's number of iterations depends on the outer loop's current state.

for (int i = 0; i < N; i++) {
    for (int j = 0; j < i; j++) {
        // constant time work
    }
}

Let's trace the inner loop's executions:

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

The total number of operations is the sum 0 + 1 + 2 + ... + (N-1). This is the sum of an arithmetic series, which equals (N-1) * N / 2.
After expanding this to (N² - N) / 2, and applying our simplification rules (dropping constants and lower-order terms), we are left with O(N²).

A helpful way to visualize this is that the operations form a triangle, which is roughly half of an N-by-N square.

Time complexity of nested for-loop

This Stack Overflow answer provides an excellent visual and mathematical explanation for why this triangular pattern results in O(N²).

Read the top three answers. The first provides a great visual analogy (the triangle of 'O's). The second and third provide the clear mathematical breakdown of the sum 1 + 2 + ... + n.

Test your understanding!

What is the time complexity of the following code snippet?

int n = 1000;
int sum = 0;

// Loop A
for (int i = 0; i < n; i++) {
    sum++;
}

// Loop B
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        sum++;
    }
}
  1. O(N)
  2. O(N log N)
  3. O(N²)
  4. O(N³)
Show answer

The correct answer is 3. O(N²).

Loop A is sequential and runs n times, so its complexity is O(N).
Loop B is a nested loop where both loops run n times, so its complexity is O(N * N) = O(N²).

The total complexity is O(N) + O(N²). When we apply the rule of dropping lower-order terms, the O(N) is insignificant compared to O(N²). Therefore, the overall complexity is O(N²).


3. Nuances and Common Pitfalls

As an experienced engineer, it's crucial to be aware of complexities that aren't immediately obvious from a simple loop counter.

1. Complexity of Function Calls:
A line of code inside a loop might look simple but could conceal a complex operation.

for (int i = 0; i < N; i++) {
    // This is not O(1)! It's O(M log M) where M is the size of someList.
    Collections.sort(someList);
}

Always consider the complexity of any function or method you call within a loop. Slicing an array, finding an element in a list, or sorting are common examples of non-O(1) operations.

2. Multiple, Unrelated Input Variables:
When an algorithm has multiple inputs (e.g., two arrays of different sizes N and M, or a grid of H rows and W columns), the complexity must reflect all variables.

  • O(N + M) for sequential processing.
  • O(N * M) for nested processing.

The "Big O Notation & Time Complexity Analysis Tutorial" video you started earlier covers these advanced cases with excellent examples.

Big O Notation & Time Complexity Analysis Tutorial

Let's revisit the Tech With Tim video to see these nuanced cases in action.

Please watch the following segments to see how to handle more complex scenarios: Two Independent Inputs (O(N+M)): 31:36 - 33:42 Nested Lists (O(H*W)): 33:42 - 37:04 Hidden Complexity in Built-in Functions: 40:44 - 43:35 and 46:47 - 51:14 Cubic Complexity (O(N³)): 57:02 - 01:00:12 Focus on how the analysis combines the basic rules we've discussed.


Conclusion

You now have a robust toolkit for analyzing the time complexity of iterative algorithms. This skill is non-negotiable for coding interviews, as it allows you to reason about your code's performance and justify your design choices to the interviewer.

Key Takeaways:

  • Sequential Code: Add the complexities and keep the dominant term.
  • Nested Code: Multiply the complexities of the loops.
  • Dependent Loops: A triangular iteration pattern (for i... for j<i...) results in O(N²).
  • Look Deeper: Always account for the complexity of function calls inside your loops. Don't assume they are O(1).
  • Multiple Inputs: Express complexity in terms of all relevant input size variables (e.g., O(N+M), O(H*W)).

Next Lesson Preview:
We've thoroughly covered how to analyze the time an algorithm takes. The other critical resource is memory. In the next lesson, we will derive Big-O space complexity by identifying memory allocations relative to input size. This will complete your foundational understanding of complexity analysis.