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 Notation: Analyzing Algorithm Complexity

Recommended resources

Welcome to the first lesson in your course on software engineering fundamentals. As we begin the "Data Structures & Algorithms Fundamentals" module, our first goal is to build the bedrock upon which all analysis of algorithms rests: understanding performance. As a senior engineering manager, you're constantly evaluating technical trade-offs. This lesson will give you the formal language to do so with precision.

Today, you will learn to analyze the time and space complexity of algorithms using Big-O notation. We will explore why this abstract measure is more useful than simply timing code with a stopwatch, establish the rules for calculating it, and then tour the most common complexity classes you'll encounter in practice. This framework is essential for making informed decisions about which algorithms and data structures are suitable for a given problem, especially as systems scale.

Why Do We Need a Formal Measure for Efficiency?

Before we dive into the "how," let's establish the "why." You might wonder why we don't just measure an algorithm's speed in seconds. The problem is that the time taken to run a program depends on many factors: the CPU speed, the amount of RAM, and even other programs running on the machine. An algorithm might take 2 seconds on an old laptop but only 1 second on a new one. This makes direct time measurement an unreliable, apples-to-oranges comparison.

We need a way to talk about an algorithm's efficiency that is independent of the hardware. This is where Big-O notation comes in. It doesn't measure time in seconds; instead, it describes how the number of operations an algorithm performs scales as the input size grows. It measures the rate of growth of the runtime, not the absolute runtime itself.

To get a clear understanding of this crucial distinction, please watch the first part of the following video from the "take U forward" channel.

Time and Space Complexity - Strivers A2Z DSA Course

This segment explains why "time taken" is not a reliable measure and introduces time complexity as the rate at which runtime increases with input size.

Watch the section from the beginning to understand the core concept of time complexity as a rate of growth, independent of machine configuration.


What are Time and Space Complexity?

When we analyze an algorithm, we're typically concerned with two resources:

  1. Time Complexity: How the runtime of an algorithm grows as the input size (n) increases.
  2. Space Complexity: How the amount of memory an algorithm uses grows as the input size (n) increases.

Big-O notation is the mathematical language we use to express both. It provides an upper bound on the growth rate, which is why it's so useful for predicting performance in the worst-case scenario.

For a formal definition, let's turn to a helpful article from DesignGurus.

Demystifying Big-O Notation: The Ultimate Guide to ...

This reading provides concise definitions for Big-O, time complexity, and space complexity.

Please read the introductory sections to solidify your understanding of these core terms. Start from the beginning and read through the definitions of time and space complexity.


The Rules of Calculation

Calculating Big-O might seem complex, but it's guided by a few straightforward rules that simplify the process. We're interested in the "big picture" of how an algorithm scales, so we can ignore the details that don't significantly impact its growth curve for large inputs.

The "take U forward" video explains these rules very clearly.

Time and Space Complexity - Strivers A2Z DSA Course

This section introduces Big-O notation and lays out the three fundamental rules for its calculation.

Please watch from this timestamp to learn the three rules: Analyze the worst-case scenario: We plan for the most pessimistic outcome to ensure our system is robust. Ignore constants: O(2n) is simplified to O(n) because the constant factor '2' doesn't change the fundamental linear growth. Drop non-dominant terms: O(n^2 + n) becomes O(n^2) because for large n, the n^2 term grows so much faster that it dominates the runtime.

An important note on space complexity: it is generally defined as the sum of input space (the memory used to store the input) and auxiliary space (any extra memory the algorithm needs). In many contexts, especially interviews, "space complexity" refers only to the auxiliary space. It's a good practice to clarify this if ambiguity arises. A crucial rule of thumb in software engineering is to avoid modifying the input data unless explicitly told to. It's better to use extra space (auxiliary space) than to risk corrupting data that might be used elsewhere in the system.


A Tour of Common Complexity Classes

Now, let's explore the most common Big-O complexities, from most efficient to least efficient. Understanding this hierarchy is key to evaluating algorithms.

This chart illustrates how the number of required operations (y-axis) scales with the input size (x-axis) for different complexity classes. Notice how quickly functions like O(n²) and O(2^n) grow compared to more efficient ones like O(log n) and O(n).

The following video from NeetCode provides a fantastic, practical tour of these complexities with code examples. As you watch, pay attention to how different code structures (loops, nested loops, recursion) map to different Big-O classes.

Big-O Notation - For Coding Interviews

This video covers the most important Big-O complexity classes with clear explanations and examples relevant to software engineering and coding interviews.

Please watch the entire video from the beginning. It covers: O(1) - Constant Time: Runtime is independent of input size. (e.g., hash map lookup) O(n) - Linear Time: Runtime grows linearly with input size. (e.g., iterating an array) O(n²) - Quadratic Time: Runtime grows by the square of the input size. (e.g., nested loops over the same input) O(log n) - Logarithmic Time: Runtime grows very slowly. Common in algorithms that repeatedly halve the problem size (e.g., binary search). O(n log n) - Linearithmic Time: A very common and efficient complexity for sorting algorithms. O(2^n) - Exponential Time: Runtime doubles with each addition to the input. Often seen in certain types of recursion. O(n!) - Factorial Time: Extremely slow. Grows incredibly fast.

Let's quickly analyze the time and space complexity of a few key examples from the resources.

1. Linear Search

This algorithm iterates through an array to find an element.

def linear_search(arr, x):
  for i in range(len(arr)):
    if arr[i] == x:
      return i
  return -1
  • Time Complexity: . In the worst case, we have to check every single one of the elements.
  • Space Complexity: . We only use a few variables (i, x), and the memory required does not grow with the input array's size.

2. Bubble Sort (Simple version)

This algorithm uses nested loops to sort an array.

def bubble_sort(arr):
  n = len(arr)
  for i in range(n):
    for j in range(0, n - i - 1):
      if arr[j] > arr[j + 1]:
        arr[j], arr[j + 1] = arr[j + 1], arr[j]
  • Time Complexity: . The outer loop runs times, and the inner loop runs approximately times for each outer iteration. This nested structure leads to quadratic complexity.
  • Space Complexity: . The sorting is done "in-place," meaning we are modifying the original array and not using any extra space that scales with the input size.

3. Recursive Fibonacci

This is a classic (though inefficient) example of calculating Fibonacci numbers.

def fibonacci(n):
  if n <= 1:
    return n
  else:
    return fibonacci(n - 1) + fibonacci(n - 2)
  • Time Complexity: . Each call to fibonacci(n) generates two more calls. This creates a recursive tree that grows exponentially.
  • Space Complexity: . The space is determined by the maximum depth of the recursion call stack. To calculate fibonacci(n), the call stack can go as deep as n, storing the state for each call.

A Practical Rule of Thumb

In competitive programming and online coding platforms, there's a useful heuristic: a typical server can perform about operations per second. This gives you a concrete way to estimate if your algorithm is fast enough.

  • If your input size N is , an algorithm would require roughly operations, which is too slow (it would take ~100 seconds).
  • An algorithm would be about , which is roughly operations—well within the one-second limit.

This shows why choosing an algorithm with a lower time complexity is not just an academic exercise; it has direct, practical consequences on performance.


Conclusion

You now have the fundamental tools to analyze algorithm performance. Let's summarize the key takeaways:

  • Big-O measures scalability, not speed. It describes the rate of growth of an algorithm's time or space requirements as the input size n increases.
  • We simplify by focusing on what matters. We analyze the worst-case scenario and drop constants and non-dominant terms to understand the overarching growth trend.
  • There is a clear hierarchy of efficiency. Algorithms with complexities like or are highly efficient and scale well. Those with complexities like are acceptable for small inputs but become slow quickly. Exponential and factorial time complexities are generally impractical for anything but very small inputs.

This cheat sheet provides a great summary of the complexities for common data structure operations and sorting algorithms, which you will encounter throughout this course.

This cheat sheet is a valuable reference, connecting the theoretical Big-O classes to their practical application in common data structures and algorithms.

In our next lesson, we will begin applying this knowledge. We will dive into the first group of fundamental data structures—arrays, linked lists, and hash maps—and use Big-O notation to compare their trade-offs for various operations.