What Is Big O Notation? Time and Space Complexity Explained
An organized book versus a shuffled deck: a visual representation of how different search algorithms scale as data grows in Big O notation.

What Is Big O Notation? Time and Space Complexity Explained

This article is part of the Data Structures & Algorithms with Python roadmap, and it’s the right place to start if you’re new to analyzing code efficiency.

Table of Contents

You write a Python function. It works fine on your test data. Then you run it on real data, and it crawls. Nothing crashes. It just takes forever. This happens to almost every developer at some point. The code is correct, but it is slow, and you do not know why. Big O notation is the tool that explains this. It tells you how your code’s speed and memory use change as your input grows, before you ever run it.

This guide is for Python learners, developers preparing for interviews, and anyone who wants to write code that stays fast as data grows. By the end, you will know what Big O means, how to calculate it for your own code, and how to spot the common mistakes people make when they use it.

In Simple Terms

Big O notation describes how the running time or memory use of an algorithm grows as the input size grows. It does not tell you exact seconds or exact bytes. It tells you the shape of growth: does the work stay flat, grow slowly, or explode as input gets bigger?

What Is Big O Notation?

Let’s understand Big O with a simple example.

Imagine you have a phone book with the names of one million people, and you want to find Alice.

Would you start from the first page and check every name one by one?

Probably not.

You would open the phone book somewhere near the middle. If you went too far, you would go back toward the beginning. If you were too early, you would move toward the end. Each time, you are cutting down the number of pages you need to search.

Because you are removing about half of the possibilities each time, you can find Alice in surprisingly few steps — even if the phone book contains a huge number of names.

Now imagine a different situation.

You have a shuffled, face-down deck of cards, and you need to find one specific card.

There is no easy shortcut. You have to pick up the cards one by one and check them.

If the card is near the top, you get lucky.

But if the card is the very last one, you have to check every card.

These are both search problems, but they behave very differently as the amount of data increases.

That’s where Big O notation comes in.

So, what exactly is Big O?

Big O notation is a way of describing how the amount of work an algorithm needs grows as the input becomes larger.

The input size is usually represented by n.

For example, if you have an array containing 100 elements, then:

n = 100

If the array contains 1,000 elements:

n = 1000

Big O helps us answer a very important question:

“What happens to my algorithm when the amount of data becomes really large?”

It focuses on the growth of the algorithm, rather than the exact number of seconds it takes to run.

Phone-Book-vs-Shuffled-Deck-1024x658 What Is Big O Notation? Time and Space Complexity Explained
Binary search vs linear search explained visually. Searching a phone book by narrowing to the middle takes about 10 steps, while searching a shuffled deck one card at a time can take every single card, the difference between O(log n) and O(n) time complexity.

Phone book vs. shuffled deck

The phone book example is similar to binary search.

Every time we look at the middle, we can eliminate roughly half of the remaining possibilities.

This gives us:

O(log n)

The shuffled deck example is similar to linear search.

We may have to check every card, one after another.

This gives us:

O(n)

The phone book example works because the names are arranged in alphabetical order. Without that ordering, you couldn’t simply jump to the middle and eliminate half the search space.

The difference becomes much more important when the amount of data gets larger.

For example, with around one million items:

  • O(n) might require checking up to about 1,000,000 items.
  • O(log n) requires only around 20 steps when the search space is repeatedly cut in half.

That’s a huge difference.

Big O is about growth, not exact speed

There is an important detail here.

Suppose one algorithm takes 3n operations and another takes n operations.

Both are considered:

O(n)

Why?

Because as n gets larger, both algorithms grow linearly.

Big O is interested in the overall growth pattern, not the exact constant in front of it.

So:

3n → O(n)

100n → O(n)

n + 50 → O(n)

The constants and smaller terms become less important when we are looking at very large input sizes.

Big O is not the same as “worst case”

This is something students often get confused about.

You may hear someone say:

“Big O means worst-case complexity.”

That’s not quite correct.

Big O describes how something grows.

Worst case describes which situation we are analyzing.

These are two different ideas.

For example, when searching through an array:

  • The item might be the first element → best case.
  • The item might be somewhere in the middle → average case.
  • The item might be the last element or not exist at all → worst case.

We can use Big O notation to describe any of these cases.

In practice, however, programmers often talk about the worst-case Big O complexity, because knowing the maximum amount of work an algorithm might require is useful when designing software.

What does Big O ignore?

Big O intentionally leaves out some details.

1. Exact hardware speed

A program might run faster on a powerful computer than on an old computer.

Big O doesn’t care about the exact speed of the computer. It describes how the algorithm’s work grows.

2. Constant factors

As we saw earlier, n, 3n, and 100n are all O(n).

The constant affects actual performance, but it doesn’t change the overall growth pattern.

3. Small inputs

Big O is mainly concerned with what happens when n becomes large.

For a very small input, an O(n²) algorithm might actually run faster than an O(n) algorithm because of differences in implementation and overhead.

The main idea to remember

Don’t try to memorize Big O as a complicated mathematical formula.

Think of it as a way to answer this question:

“As my input gets bigger, how quickly does the amount of work my algorithm has to do grow?”

For example:

O(1) → The amount of work stays roughly the same.

O(log n) → The work grows very slowly.

O(n) → The work grows directly with the input.

O(n²) → The work grows much faster as the input gets bigger.

That’s the basic idea behind Big O notation.

Once you understand this idea, learning how to calculate the Big O of actual code becomes much easier.

Why Big O Matters in DSA

Now that we understand what Big O notation is, you might be wondering:

“Why do we actually need it?”

If an algorithm gives us the correct answer, why should we care about its Big O?

The answer becomes clear when the amount of data gets large.

An algorithm that works perfectly with 10 or 100 items might become extremely slow when it has to deal with 1 million or 100 million items.

This is one of the main reasons Big O notation is so important in Data Structures and Algorithms (DSA).

1. It helps us compare algorithms

Imagine you have two different algorithms that solve the same problem.

One takes O(n) time, while the other takes O(n²) time.

For a small input, you might not notice much difference.

But as the input grows, the difference becomes huge.

For example, if n = 1,000:

  • O(n) → about 1,000 units of work
  • O(n²) → about 1,000,000 units of work

Both algorithms solve the same problem, but one scales much better than the other.

Big O gives us a simple way to compare them.

2. It helps us choose the right data structure

Data structures are not all good at the same things.

For example, suppose you need to search for a value.

Depending on the situation, you might use an:

  • Array
  • Linked list
  • Hash table
  • Binary search tree

Each one can have different performance characteristics.

Big O helps us understand those differences and choose a data structure that fits the problem.

3. It helps us write scalable programs

A program might work perfectly on your computer with a few hundred records.

But what happens when your application has millions of users?

This is where algorithm efficiency really matters.

A poorly chosen algorithm can make an application slow, expensive, or even unusable when the amount of data increases.

By understanding Big O, we can think ahead and choose algorithms that are more likely to handle large inputs efficiently.

4. It helps us identify inefficient code

Big O can also help you look at your own code and ask:

“Is there a better way to do this?”

For example, suppose you have a loop inside another loop.

That might result in O(n²) time complexity.

Sometimes that’s perfectly acceptable.

But if n can become very large, you might look for a way to solve the same problem in O(n) or O(n log n) time.

So Big O isn’t just something you calculate for an exam. It is a way of thinking about the efficiency of your code.

5. It is important for technical interviews

If you are preparing for software engineering interviews, you will almost certainly come across Big O.

Interviewers often ask questions such as:

  • What is the time complexity of this code?
  • What is the space complexity?
  • Can you make this solution more efficient?
  • What happens if the input becomes very large?

They aren’t only checking whether you can write code.

They also want to know whether you understand why your solution is efficient.

6. It helps us think beyond “Does it work?”

When you’re learning programming, your first goal is usually:

“Can I make this code work?”

That’s a good starting point.

But DSA takes you one step further:

“Can I make it work efficiently?”

Two algorithms can produce exactly the same result, but one can require dramatically less time or memory.

Big O helps us understand that difference.

A simple example

Suppose you need to find a person’s name in a collection of names.

A simple approach might check every name one by one.

That’s O(n).

But if the data is organized in a way that allows you to repeatedly eliminate half of the possibilities, you might be able to search in O(log n).

Both approaches can find the answer.

The difference is how much work they need as the data grows.

That’s the real reason Big O matters.

The bigger picture

DSA is not about memorizing a list of Big O values.

It is about learning how to make better decisions.

When you understand Big O, you start asking questions like:

“How much data will this program have?”

“How will this algorithm behave when the data becomes much larger?”

“Can I solve this problem with less time or memory?”

That way of thinking is what makes Big O such an important part of DSA.

In short

Big O matters because it helps us understand, compare, and improve the efficiency of algorithms.

It helps us move from simply writing programs that work to writing programs that scale well.

And once you understand Big O, you’ll have a much easier time understanding why certain data structures and algorithms are preferred over others.

Time Complexity vs Space Complexity

When we talk about the efficiency of an algorithm, there are two main things we usually care about:

  1. How much time does it take?
  2. How much memory does it use?

These are called time complexity and space complexity.

Let’s understand them one at a time.

What is Time Complexity?

Time complexity describes how the amount of work an algorithm needs grows as the input size increases.

It doesn’t mean the exact number of seconds your program takes.

For example, suppose you have an array containing n numbers and you want to print every number.

for each number in the array:
    print(number)

If there are 10 numbers, you print 10 numbers.

If there are 1,000 numbers, you print 1,000 numbers.

The amount of work grows along with the input.

So the time complexity is:

O(n)

The important thing is not the exact time. A computer might process those 1,000 numbers very quickly.

What matters is that if the input becomes 10 times larger, the amount of work also becomes roughly 10 times larger.

What is Space Complexity?

Space complexity describes how much additional memory an algorithm needs as the input size increases.

For example, imagine you have an algorithm that creates a new array containing n elements:

newArray = new array of size n

As n gets larger, the amount of memory needed also increases.

So the space complexity is:

O(n)

In other words, the algorithm needs more memory as the input grows.

A simple real-life example

Think about cooking in a kitchen.

Suppose you are preparing a meal for 2 people.

You might need:

  • A few ingredients
  • One cutting board
  • One pan

Now imagine you’re preparing the same meal for 100 people.

You will probably need more ingredients, more containers, and possibly more cooking equipment.

The time needed to prepare the food and the space needed to store and handle everything can both increase.

Algorithms work in a similar way.

Time vs Space: The key difference

The easiest way to remember the difference is:

Time complexity = How much work does the algorithm do?

Space complexity = How much extra memory does the algorithm need?

For example, consider this algorithm:

sum = 0

for each number in the array:
    sum = sum + number

The algorithm looks at every element once.

Therefore:

Time complexity: O(n)

But notice that we only use a few extra variables such as sum.

We don’t create another array containing all the elements.

Therefore:

Space complexity: O(1)

So this algorithm is:

Time → O(n)
Space → O(1)

Why do we care about both?

Sometimes an algorithm is very fast but uses a lot of memory.

Other times, an algorithm uses very little memory but takes more time.

For example, you might be able to make a program faster by storing some previously calculated results in memory.

This technique is often called memoization or caching.

You are basically saying:

“I’ll use some extra memory so I don’t have to do the same work again.”

This creates a common trade-off:

More memory → potentially less time

or

Less memory → potentially more time

This is known as a time-space trade-off.

Example: Searching for an element

Suppose you want to find a number in a collection.

One approach is to check each element one by one.

That could take:

Time: O(n)

And if you don’t create any additional data structure:

Extra Space: O(1)

Another approach might use a hash table to store information that makes searching much faster.

Depending on the exact implementation, searching can be approximately:

Time: O(1) on average

But now you need additional memory to store the hash table:

Space: O(n)

So you have exchanged some extra memory for faster lookups.

One important detail: input space vs. auxiliary space

When discussing space complexity, you’ll sometimes hear the terms input space and auxiliary space.

The input itself already takes some memory.

For example, if a function receives an array containing n elements, that array already occupies memory.

When we talk about auxiliary space, we’re usually asking:

“How much extra memory does the algorithm need besides the input?”

This distinction is useful because it tells us whether the algorithm itself is creating additional memory.

For example:

function findMax(array):
    max = array[0]

    for each number in array:
        if number > max:
            max = number

The input array contains n elements, but the algorithm only uses a few extra variables.

So its:

Time complexity → O(n)

Auxiliary space → O(1)

Quick comparison

ComplexityWhat does it measure?Example
Time ComplexityHow the amount of work growsSearching through n elements → O(n)
Space ComplexityHow memory usage growsCreating an array of n elements → O(n)
Auxiliary SpaceExtra memory used by the algorithmA few variables → O(1)

The main idea

When analyzing an algorithm, don’t just ask:

“Does this code work?”

Also ask:

“How much time will it need when the input becomes large?”

and:

“How much additional memory will it need?”

That’s why we analyze both time complexity and space complexity.

A good algorithm isn’t necessarily the one that uses the least time or the least memory. The goal is usually to find a reasonable balance between the two based on the problem you’re trying to solve.

O(1), O(log n), O(n), O(n log n), and O(n²) Explained

Now that we understand what Big O notation means, let’s look at the most common time complexities you’ll encounter in DSA.

Don’t worry if these look like complicated mathematical expressions at first. The easiest way to understand them is to see how the amount of work changes when the input gets bigger.

Here are the five complexities we’ll focus on:

  • O(1) — Constant Time
  • O(log n) — Logarithmic Time
  • O(n) — Linear Time
  • O(n log n) — Linearithmic Time
  • O(n²) — Quadratic Time

Let’s understand each one with a simple explanation and a Python example.


1. O(1) — Constant Time

Let’s start with the simplest one.

O(1) means that the amount of work stays roughly the same, regardless of how large the input becomes.

For example, suppose we have a Python list and want to get its first element:

numbers = [10, 20, 30, 40, 50]

print(numbers[0])

We don’t need to look through the entire list.

Python can directly access the element at index 0.

Whether the list contains 5 elements or 5 million elements, accessing that particular position takes roughly the same amount of work.

So the time complexity is:

O(1)

Real-life example

Imagine a classroom with 10 students or 1,000 students.

If I tell you:

“Go to seat number 5.”

You don’t need to check every student to find that seat.

That’s the basic idea behind constant time.

Remember

O(1) → The input can grow, but the work stays roughly the same.


2. O(log n) — Logarithmic Time

Now let’s look at O(log n).

This is a little more interesting.

The basic idea is:

The algorithm reduces the problem significantly at every step, often by half.

A classic example is binary search.

Imagine you have a sorted list:

numbers = [10, 20, 30, 40, 50, 60, 70, 80, 90]

Suppose you want to find 70.

Instead of checking every number from the beginning, binary search looks at the middle.

If the middle value is too small, we ignore the entire left half.

Then we repeat the process with the remaining half.

Here’s a simple Python implementation:

def binary_search(numbers, target):
    left = 0
    right = len(numbers) - 1

    while left <= right:
        middle = (left + right) // 2

        if numbers[middle] == target:
            return middle
        elif numbers[middle] < target:
            left = middle + 1
        else:
            right = middle - 1

    return -1

The important part isn’t memorizing the code yet.

Focus on what happens:

We keep cutting the search area in half.

That’s why binary search has:

O(log n) time complexity.

Real-life example

Think about searching for a word in a dictionary.

You don’t start at page 1 and read every word.

You open somewhere in the middle.

Then you decide whether to go left or right.

You keep eliminating large portions of the dictionary.

That’s the idea behind O(log n).

Remember

O(log n) → The problem gets much smaller at every step.


3. O(n) — Linear Time

Now we have O(n).

This is one of the easiest complexities to understand.

O(n) means the amount of work grows directly with the size of the input.

For example:

numbers = [10, 20, 30, 40, 50]

for number in numbers:
    print(number)

If the list contains 5 numbers, we process 5 numbers.

If it contains 1,000 numbers, we process 1,000 numbers.

If it contains 1 million numbers, we may process 1 million numbers.

The amount of work grows along with n.

Therefore:

O(n)

Real-life example

Imagine a teacher checking the attendance of every student.

If there are 20 students, you check 20 students.

If there are 100 students, you check 100 students.

The more students you have, the more work you have to do.

That’s linear growth.

Remember

O(n) → If the input doubles, the work roughly doubles.


4. O(n log n) — Linearithmic Time

Now let’s combine two ideas:

O(n) and O(log n).

This gives us:

O(n log n)

You will see this complexity frequently when learning sorting algorithms.

For example, Merge Sort has O(n log n) time complexity.

Here is a simplified Python implementation:

def merge_sort(numbers):
    if len(numbers) <= 1:
        return numbers

    middle = len(numbers) // 2

    left = merge_sort(numbers[:middle])
    right = merge_sort(numbers[middle:])

    return merge(left, right)


def merge(left, right):
    result = []
    i = 0
    j = 0

    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    result.extend(left[i:])
    result.extend(right[j:])

    return result

Don’t worry if this code looks complicated right now. We’ll study sorting algorithms separately.

For now, understand the basic idea.

Merge Sort repeatedly divides the data into smaller pieces, which gives us the log n part.

Then it processes the elements while merging those pieces back together, which gives us the n part.

Together:

O(n log n)

Real-life example

Imagine you have 1,000 exam papers that need to be sorted.

Instead of dealing with all 1,000 at once, you divide them into smaller groups, organize those groups, and then combine them.

This kind of divide-and-process approach can lead to O(n log n) complexity.

Remember

O(n log n) → More work than O(n), but usually much better than O(n²).


5. O(n²) — Quadratic Time

Now we come to O(n²).

This happens when the amount of work grows roughly with the square of the input size.

A common example is a loop inside another loop.

For example:

numbers = [1, 2, 3, 4, 5]

for i in numbers:
    for j in numbers:
        print(i, j)

The outer loop runs n times.

For every iteration of the outer loop, the inner loop also runs n times.

So:

n × n = n²

Therefore:

O(n²)

Let’s see what happens as the input grows

If:

n = 10

Then roughly:

10 × 10 = 100

operations.

If:

n = 100

Then:

100 × 100 = 10,000

operations.

If:

n = 1,000

Then:

1,000 × 1,000 = 1,000,000

operations.

This is why O(n²) algorithms can become slow when the input becomes large.

Real-life example

Imagine 100 students in a classroom.

If every student needs to talk to every other student, you can end up with a very large number of interactions.

As the number of students increases, the number of possible pairs grows quickly.

That’s the basic idea behind quadratic growth.

Remember

O(n²) → The work can grow very quickly as the input gets larger.


Comparing Them

Now let’s put everything together.

Big ONameBasic Idea
O(1)ConstantWork stays roughly the same
O(log n)LogarithmicProblem gets smaller quickly
O(n)LinearWork grows with the input
O(n log n)LinearithmicCombines linear and logarithmic growth
O(n²)QuadraticWork grows roughly as input squared

A useful way to think about their growth is:

O(1) → O(log n) → O(n) → O(n log n) → O(n²)

Generally, as you move toward the right, the algorithm becomes less scalable for very large inputs.

But remember: Big O is about growth, not a stopwatch. For small inputs, an algorithm with a theoretically worse complexity can sometimes be faster because of constants, implementation details, or overhead.


A Simple Example to Remember Everything

Imagine you have a list of students.

O(1)

You want to find the student at a specific position.

student = students[5]

You directly access the position.

O(1)

O(log n)

You search a sorted list by repeatedly cutting the search area in half.

O(log n)

O(n)

You check every student one by one.

for student in students:
    print(student)

O(n)

O(n log n)

You use an efficient divide-and-conquer sorting algorithm such as Merge Sort.

O(n log n)

O(n²)

You compare every student with every other student.

for student1 in students:
    for student2 in students:
        print(student1, student2)

O(n²)


The Most Important Thing to Understand

Don’t just memorize these five Big O values.

Try to understand how the work grows.

If you remember that, the notation becomes much easier.

Think of it this way:

O(1) → “I can do it directly.”

O(log n) → “I can keep cutting the problem down.”

O(n) → “I need to look at each item.”

O(n log n) → “I divide the problem and process the pieces efficiently.”

O(n²) → “For each item, I may have to look at many other items.”

Once you can recognize these patterns in Python code, calculating Big O becomes much easier.

Quick Complexity Reference

NotationNameExample
O(1)ConstantAccessing an item by index
O(log n)LogarithmicBinary search
O(n)LinearLooping through a list once
O(n log n)LinearithmicMerge sort, Python’s sorted()
O(n²)QuadraticNested loops over the same input
Big-O-growth-curves-1-1024x644 What Is Big O Notation? Time and Space Complexity Explained
Big O notation growth rates compared. As input size increases, O(1) stays flat, O(log n) and O(n) rise steadily, and O(n²) climbs sharply, showing why time complexity matters for algorithm performance.

How to Calculate Time Complexity

You do not need a formal proof to calculate Big O for most code. You need a simple, repeatable process. Here is one that works for the majority of everyday functions.

Step 1: Identify the input size, n. This is usually the length of a list, string, or the number of elements you are processing.

Step 2: Look at each loop. A single loop that runs through all n items is O(n). A loop that runs a fixed number of times, like exactly 5 times regardless of input, does not depend on n, so it counts as O(1).

Step 3: Look at nested loops. If a loop sits inside another loop, and both depend on the same input size, multiply their complexities. Two nested loops over the same n items give O(n) times O(n), which is O(n²).

Step 4: Look at loops that shrink the problem. If a loop or recursive call cuts the problem size in half (or by some fraction) each time, that is a sign of O(log n).

Step 5: Add up separate, non-nested steps. If your function does one O(n) loop, then a separate O(n) loop afterward (not nested), that is O(n) + O(n), which simplifies to O(n). Big O only cares about the fastest-growing term, so you drop constants and lower-order terms.

Step 6: Keep the largest term, and drop the rest. If a function does O(n²) work in one part and O(n) work in another, the overall complexity is O(n²), because that term dominates as n grows.

Time-Complexity-Calculation-Steps-820x1024 What Is Big O Notation? Time and Space Complexity Explained
How to calculate time complexity step by step. Identify n, check loops, check nested loops, spot loops that shrink the problem, add separate steps, then keep the largest term to find your Big O.

Let’s walk through a real example:

def process(items):
    total = 0                      # O(1)
    for item in items:             # O(n)
        total += item

    seen = set()
    for item in items:             # O(n), separate from the loop above
        seen.add(item)

    return total, seen

Two separate O(n) loops, one after another. That adds up to O(n) + O(n) = O(2n), and Big O drops the constant, giving you O(n) overall.

Now compare it to this:

def process_pairs(items):
    pairs = []
    for i in items:                # O(n)
        for j in items:            # O(n), nested inside the first loop
            pairs.append((i, j))
    return pairs

Here the second loop is nested inside the first, so you multiply: O(n) times O(n) = O(n²).

How to Calculate Space Complexity

Calculating space complexity uses the same mindset, but you track memory instead of steps. Ask yourself: as n grows, does the amount of extra memory this function uses grow too?

Step 1: Ignore the input itself. If a function receives a list as a parameter, that list already exists in memory before the function runs. Space complexity usually measures the additional memory the function creates.

Step 2: Look for new data structures. If the function creates a new list, set, or dictionary that grows with the input, that is O(n) space.

Step 3: Look at the call stack for recursion. Every recursive call adds a new frame to the call stack, and that frame stays in memory until the call returns. A recursive function that calls itself n times uses O(n) space, even if it never creates a single list or dictionary.

Here is a recursive example that shows this clearly:

def countdown(n):
    if n <= 0:
        return
    countdown(n - 1)

This function does not create any list or dictionary. It looks like it should use no extra memory. But each call to countdown stays on the call stack until the recursion below it finishes. Calling countdown(1000) creates 1,000 stacked function calls before any of them return.

It helps to break the total space down into parts:

  • Recursive call stack: O(n), one frame for each active call
  • Explicit data structures created: O(1), since no list, set, or dictionary is created
  • Total auxiliary space: O(n), because the call stack dominates
Call-Stack-Space-Growth-in-Recursion-1024x644 What Is Big O Notation? Time and Space Complexity Explained
Call stack growth in recursion explained. Each call to countdown(n) pushes a new frame onto the stack, and by the time countdown(0) is reached, all 5 frames are stored in memory at once, this is O(n) space complexity.

Compare that to the earlier sum_of_list function. It loops instead of recursing, so it never builds up a call stack, and it uses O(1) space overall.

This is one of the most overlooked ideas in space complexity: recursion is not “free” just because you did not explicitly create a data structure. The call stack itself counts as memory.

Common Big O Mistakes

These mistakes show up constantly, even among developers who understand the concept in theory.

Mistake 1: Confusing “fast in testing” with “fast at scale.” Code that runs quickly on a list of 20 items can still be O(n²). Small inputs hide bad complexity. Always ask “what happens if this input were 100 times bigger?” instead of trusting a quick test run.

Mistake 2: Assuming every nested loop means O(n²). This is not always true. If the inner loop does not depend on the size of the outer loop’s input, and instead runs a fixed number of times, it does not multiply the way you’d expect.

def print_first_three(items):
    for item in items:              # O(n)
        for i in range(3):          # Always exactly 3 iterations, not O(n)
            print(item, i)

This is O(n), not O(n²), because the inner loop always runs exactly 3 times, regardless of n.

The safest way to check is to ask: “does the inner loop’s range depend on the input size, or is it fixed?” Sometimes the inner loop does depend on the input, but not in a way that repeats the full outer loop. For example:

def print_triangle(items):
    for i in range(len(items)):
        for j in range(i):
            print(items[i], items[j])

Here the inner loop’s range depends on i, so it is not a fixed number like the last example. But it is also not a full n iterations every time. The inner loop runs 0 times, then 1, then 2, and so on, up to n - 1. Added together, that totals roughly n² / 2 operations, which still simplifies to O(n²), since Big O drops constants. The lesson here is to actually trace through what each loop does, instead of assuming “nested loop always means O(n²)” or “nested loop never means O(n²).” Both assumptions can be wrong. Read the loop bounds.

Mistake 3: Forgetting that built-in operations have their own cost. It is easy to assume every line of code is O(1). But operations like in on a Python list, or inserting at the front of a list, are O(n) themselves, because Python has to check or shift every element.

def contains(items, target):
    return target in items   # O(n) on a list, not O(1)

Checking membership in a set or dict, however, is typically O(1) on average, because of how hashing works. This single difference (list vs set for membership checks) is one of the most common causes of accidentally slow Python code.

Mistake 4: Ignoring space complexity entirely. Many developers only think about time. But an algorithm that is fast in time and terrible in memory can crash a program on large inputs just as easily as a slow one can time out.

Mistake 5: Treating Big O as an exact prediction of runtime. Big O tells you about growth rate, not literal seconds. Two O(n) functions can run at very different real speeds, because of constants, hardware, and implementation details. Big O tells you the shape of the curve, not the exact time on the clock.

Big O Examples in Python

A few more grounded, side-by-side examples, since seeing the pattern repeated in different situations helps it stick.

# O(1): Constant time
def get_last_item(items):
    return items[-1]

# O(log n): Logarithmic time
def power_of_two_search(n):
    count = 0
    while n > 1:
        n = n // 2
        count += 1
    return count

# O(n): Linear time
def find_max(items):
    max_value = items[0]
    for item in items:
        if item > max_value:
            max_value = item
    return max_value

# O(n log n): Linearithmic time
def sorted_copy(items):
    return sorted(items)

# O(n²): Quadratic time
def all_pairs_sum(items):
    results = []
    for i in items:
        for j in items:
            results.append(i + j)
    return results

Reading through code like this, and asking “what is n here, and how does the work scale with it,” is the fastest way to build real intuition for Big O.

Big O Cheat Sheet

ComplexityIntuitionTypical example
O(1)Doesn’t grow with inputList indexing
O(log n)Grows very slowlyBinary search
O(n)Grows proportionally with inputLinear scan
O(n log n)Slightly faster-growing than linearEfficient sorting
O(n²)Grows rapidlyNested loops, pairwise comparisons
Big-O-Cheat-Sheet-1024x619 What Is Big O Notation? Time and Space Complexity Explained
Big O notation cheat sheet. Quick reference table comparing constant, logarithmic, linear, linearithmic, and quadratic time complexity, from list indexing to nested loops.

As a rule of thumb: O(1) and O(log n) tend to stay manageable even as input grows very large. O(n) and O(n log n) scale reasonably well for most real-world data sizes. O(n²) and worse are the ones to watch closely, since the actual runtime can grow rapidly once your input reaches the thousands or millions. This is about growth trend, not a guarantee about literal speed. A constant-time operation with heavy overhead can still be slower in practice than a linear one with very little overhead, especially on small inputs.

Big O Interview Questions for FAANG-Style DSA Interviews

If you’re preparing for DSA interviews at companies such as Google, Amazon, Meta, Apple, or Microsoft, you should be comfortable analyzing the time and space complexity of your Python code. Interviewers often care not only about getting the correct answer, but also about explaining how efficiently your solution scales.

Here are some important interview questions to practice.

1. What is Big O notation?

Answer:

Big O notation describes how an algorithm’s time or memory requirements grow as the input size n increases.

For example:

  • O(1) → Constant
  • O(log n) → Logarithmic
  • O(n) → Linear
  • O(n log n) → Linearithmic
  • O(n²) → Quadratic

Big O focuses on the growth rate, rather than the exact execution time.


2. What is the time complexity of this Python code?

numbers = [1, 2, 3, 4, 5]

for number in numbers:
    print(number)

Answer:

The loop visits every element once.

If there are n elements, the loop runs n times.

Time Complexity: O(n)

Space Complexity: O(1) if we don’t count the input list itself.


3. What is the time complexity of nested loops?

numbers = [1, 2, 3, 4, 5]

for i in numbers:
    for j in numbers:
        print(i, j)

Answer:

The outer loop runs n times.

For every iteration of the outer loop, the inner loop also runs n times.

Therefore:

n × n = n²

Time Complexity: O(n²)

Space Complexity: O(1)


4. What is the time complexity of accessing an element by index in a Python list?

numbers = [10, 20, 30, 40, 50]

print(numbers[3])

Answer:

Python lists provide direct access by index.

We don’t need to search through the previous elements.

Time Complexity: O(1)

Space Complexity: O(1)


5. What is the time complexity of searching for a value in a Python list?

numbers = [10, 20, 30, 40, 50]

if 40 in numbers:
    print("Found")

Answer:

Python may need to check each element until it finds the value.

In the worst case, it checks all n elements.

Time Complexity: O(n)

Space Complexity: O(1)


6. What is the time complexity of binary search?

Answer:

Binary search repeatedly divides the search space into half.

For example:

n → n/2 → n/4 → n/8 → ...

Because the search space is reduced by half at every step, the time complexity is:

O(log n)

Binary search requires the data to be appropriately sorted.


7. What is the difference between O(n) and O(log n)?

Answer:

With O(n), the algorithm may need to process every element.

With O(log n), the algorithm can eliminate a large portion of the input at each step.

For example:

Linear search → O(n)

Binary search → O(log n)

As n becomes very large, O(log n) grows much more slowly than O(n).


8. What is the time complexity of this code?

numbers = [1, 2, 3, 4, 5]

for i in range(len(numbers)):
    for j in range(i + 1, len(numbers)):
        print(numbers[i], numbers[j])

Answer:

There are two nested loops, and the number of iterations grows approximately with the square of n.

Therefore:

Time Complexity: O(n²)

Space Complexity: O(1)

The exact number of iterations is smaller than n², but Big O ignores constant factors and lower-order terms.


9. What is the time complexity of Python’s sort()?

numbers.sort()

Answer:

Python’s built-in list sorting uses Timsort.

Its worst-case time complexity is:

O(n log n)

Python’s official documentation provides the complexity details for built-in data structures and operations, making it a useful reference when analyzing Python code.


10. What is the difference between time complexity and space complexity?

Answer:

Time complexity describes how the amount of computational work grows as the input becomes larger.

Space complexity describes how the memory requirements grow.

For example:

def double_numbers(numbers):
    result = []

    for number in numbers:
        result.append(number * 2)

    return result

The function processes every element once.

Time Complexity: O(n)

But it also creates a new list containing n elements.

Auxiliary Space: O(n)


11. Can an algorithm have O(n) time and O(1) space?

Answer:

Yes.

For example:

def find_max(numbers):
    maximum = numbers[0]

    for number in numbers:
        if number > maximum:
            maximum = number

    return maximum

We examine every element once:

Time: O(n)

But we only use a few variables regardless of the input size:

Auxiliary Space: O(1)


12. Which is better: O(n) or O(n²)?

Answer:

Generally, O(n) is better for large inputs because it grows much more slowly than O(n²).

For example, when n = 1,000:

O(n) → approximately 1,000 operations

O(n²) → approximately 1,000,000 operations

However, Big O isn’t a stopwatch. For very small inputs, constants and implementation details can make the practical performance different.


13. What is the time complexity of this code?

for i in range(n):
    print(i)

for j in range(n):
    print(j)

Answer:

The first loop runs n times.

The second loop also runs n times.

So the total work is:

n + n = 2n

We drop the constant 2.

Therefore:

Time Complexity: O(n)

This is an important interview concept: two consecutive O(n) loops are still O(n), not O(n²).


14. What is the time complexity of this code?

for i in range(n):
    for j in range(n):
        print(i, j)

for k in range(n):
    print(k)

Answer:

The nested loops take:

O(n²)

The final loop takes:

O(n)

Together:

O(n² + n)

We keep the term that grows faster and drop the smaller term.

Therefore:

Time Complexity: O(n²)


15. Can you improve an O(n²) solution to O(n)?

Answer:

Sometimes, yes.

A common example is searching for duplicate values.

A brute-force approach might compare every element with every other element:

for i in range(len(numbers)):
    for j in range(i + 1, len(numbers)):
        if numbers[i] == numbers[j]:
            return True

This takes:

O(n²) time.

We can often use a Python set to remember values we’ve already seen:

seen = set()

for number in numbers:
    if number in seen:
        return True

    seen.add(number)

return False

The average-case time complexity becomes:

O(n)

But we now use additional memory:

Space Complexity: O(n)

This is a classic example of a time-space trade-off: we use more memory to reduce the amount of work.


Quick Interview Revision

Before an interview, make sure you can answer these without hesitation:

QuestionAnswer
Array/list index access?O(1)
Search in an unsorted list?O(n)
Binary search?O(log n)
One loop through n elements?O(n)
Two nested loops?O(n²)
Efficient comparison-based sorting?O(n log n)
Two separate O(n) loops?O(n)
O(n²) + O(n)?O(n²)
Store n additional elements?O(n) space
Use only a few extra variables?O(1) space

The interview habit you should develop

When you finish writing a solution, don’t stop at:

“My code works.”

Get into the habit of saying:

“The time complexity is O(n), because I traverse the input once. The auxiliary space is O(1), because I only use a constant number of extra variables.”

That one habit will make your DSA explanations much stronger in technical interviews.

Important: These are representative interview-style questions, not guaranteed questions from a particular company’s current interview pool. Exact questions vary by team, level, location, and interview. Current preparation sources consistently emphasize complexity analysis as a core DSA interview skill.

Big O Practice Problems

Now it’s time to test your understanding.

For each problem, try to answer these two questions:

  1. What is the time complexity?
  2. What is the space complexity?

Don’t look for the answer immediately. Try to analyze the code yourself first.


Problem 1 — Simple Loop

What is the time complexity?

numbers = [1, 2, 3, 4, 5]

for number in numbers:
    print(number)

Your answer: O(?)


Problem 2 — Accessing an Element

What is the time complexity?

numbers = [10, 20, 30, 40, 50]

print(numbers[2])

Your answer: O(?)


Problem 3 — Searching a List

What is the worst-case time complexity?

numbers = [10, 20, 30, 40, 50]

target = 50

for number in numbers:
    if number == target:
        print("Found")
        break

Your answer: O(?)


Problem 4 — Two Separate Loops

What is the time complexity?

numbers = [1, 2, 3, 4, 5]

for number in numbers:
    print(number)

for number in numbers:
    print(number * 2)

Your answer: O(?)

Hint: The loops are next to each other, not inside each other.


Problem 5 — Nested Loops

What is the time complexity?

numbers = [1, 2, 3, 4, 5]

for i in numbers:
    for j in numbers:
        print(i, j)

Your answer: O(?)


Problem 6 — Nested Loop with a Constant

What is the time complexity?

numbers = [1, 2, 3, 4, 5]

for number in numbers:
    for i in range(10):
        print(number, i)

Your answer: O(?)

Hint: range(10) always runs 10 times. It does not depend on n.


Problem 7 — Find the Maximum

What are the time and space complexities?

def find_max(numbers):
    maximum = numbers[0]

    for number in numbers:
        if number > maximum:
            maximum = number

    return maximum

Your answer:

Time: O(?)

Space: O(?)


Problem 8 — Creating a New List

What are the time and space complexities?

def double_numbers(numbers):
    result = []

    for number in numbers:
        result.append(number * 2)

    return result

Your answer:

Time: O(?)

Space: O(?)


Problem 9 — Using a Set

What is the average-case time complexity?

def has_duplicate(numbers):
    seen = set()

    for number in numbers:
        if number in seen:
            return True

        seen.add(number)

    return False

Your answer:

Time: O(?)

Space: O(?)

Think carefully: Why does using a set make this solution different from comparing every pair?


Problem 10 — Binary Search

Consider the idea behind binary search:

def binary_search(numbers, target):
    left = 0
    right = len(numbers) - 1

    while left <= right:
        middle = (left + right) // 2

        if numbers[middle] == target:
            return middle
        elif numbers[middle] < target:
            left = middle + 1
        else:
            right = middle - 1

    return -1

What is the time complexity?

What is the space complexity of this iterative implementation?

Your answer:

Time: O(?)

Space: O(?)


Challenge Problems

If the first 10 problems feel comfortable, try these.

Problem 11 — What’s the Complexity?

def example(numbers):
    for i in range(len(numbers)):
        for j in range(i + 1, len(numbers)):
            print(numbers[i], numbers[j])

Your answer: O(?)


Problem 12 — Three Loops

def example(numbers):
    for number in numbers:
        print(number)

    for number in numbers:
        print(number)

    for number in numbers:
        print(number)

Your answer: O(?)


Problem 13 — Nested + Separate Loop

def example(numbers):
    for i in numbers:
        for j in numbers:
            print(i, j)

    for number in numbers:
        print(number)

Your answer: O(?)


Problem 14 — Which One Is Better?

You have two solutions to the same problem.

Solution A:

def find_duplicate(numbers):
    for i in range(len(numbers)):
        for j in range(i + 1, len(numbers)):
            if numbers[i] == numbers[j]:
                return True

    return False

Solution B:

def find_duplicate(numbers):
    seen = set()

    for number in numbers:
        if number in seen:
            return True

        seen.add(number)

    return False

Answer these questions:

  1. What is the time complexity of Solution A?
  2. What is the time complexity of Solution B?
  3. What is the space complexity of Solution A?
  4. What is the space complexity of Solution B?
  5. Which solution would you prefer for a very large input?
  6. What trade-off does Solution B make?

Answers

Try solving all the problems yourself before reading this section.

Problem 1

Time: O(n)

The loop processes every element once.

Space: O(1) if we don’t count the input list.


Problem 2

Time: O(1)

Python can access a list element directly by its index.

Space: O(1)


Problem 3

Time: O(n)

In the worst case, the target can be at the end of the list, so we may need to check every element.

Space: O(1)


Problem 4

Time: O(n)

The first loop is O(n).

The second loop is also O(n).

So:

O(n) + O(n) = O(2n)

We drop the constant:

O(n)


Problem 5

Time: O(n²)

The outer loop runs n times, and the inner loop runs n times for each outer iteration.

n × n = n²


Problem 6

Time: O(n)

The inner loop always runs 10 times.

So the total work is:

n × 10 = 10n

We drop the constant 10.

O(n)


Problem 7

Time: O(n)

We examine every element once.

Space: O(1)

Only a few variables are used.


Problem 8

Time: O(n)

Every input element is processed once.

Space: O(n)

The new result list contains n elements.


Problem 9

Average Time: O(n)

Set lookup and insertion are O(1) on average, so we process n elements.

Space: O(n)

The seen set can contain up to n elements.


Problem 10

Time: O(log n)

The search space is cut roughly in half on every iteration.

Space: O(1)

This is an iterative implementation, so it doesn’t create a recursive call stack.


Problem 11

Time: O(n²)

Although the inner loop doesn’t always run exactly n times, the total number of comparisons grows quadratically.

O(n²)


Problem 12

Time: O(n)

The three loops are sequential:

O(n) + O(n) + O(n) = O(3n)

Drop the constant:

O(n)


Problem 13

Time: O(n²)

The nested loops require O(n²).

The additional loop requires O(n).

So:

O(n²) + O(n) = O(n²)

We keep the term that grows faster.


Problem 14

Solution A:

Time: O(n²)

Space: O(1)

Solution B:

Average time: O(n)

Space: O(n)

For a very large input, Solution B is generally preferable because it reduces the running time significantly by using extra memory.

This is a classic example of a:

Time-space trade-off


Interview Tip

When an interviewer gives you a piece of code and asks for its Big O, don’t guess immediately.

Follow these steps:

Step 1: Identify what n represents.

Step 2: Count how many times each loop runs.

Step 3: Check whether loops are nested or sequential.

Step 4: Look for operations such as searching, sorting, slicing, or creating new data structures.

Step 5: Simplify the final expression.

For example:

O(n² + n + 10)

becomes:

O(n²)

because n² grows faster than n and the constant 10.

The goal isn’t just to memorize Big O values. You should be able to look at Python code and explain why it has that complexity.

Frequently Asked Questions

  1. Is Big O the same as runtime in seconds?

    No. Big O describes how work grows as input grows, not the literal number of seconds a program takes. Two O(n) functions can run at different real speeds depending on hardware and implementation details.

  2. What is the difference between Big O, Big Omega, and Big Theta?

    Big O describes an asymptotic upper bound on growth. Big Omega describes an asymptotic lower bound on growth. Big Theta describes a tight bound, where the upper and lower bounds match. None of the three is inherently tied to “worst case” or “best case.” In everyday conversation, though, people usually pair Big O with worst-case behavior, because that is the number most useful for planning around. Big O is the one used most often in everyday discussion and interviews.

  3. Is O(1) always faster than O(n)?

    Not necessarily, for every input size. O(1) has a better asymptotic growth rate than O(n), meaning it scales better as input grows toward infinity. But actual runtime also depends on constants, implementation details, and hardware, not growth rate alone. A constant-time operation with a lot of overhead can still be slower than a linear operation on a small input. Big O tells you which one wins as n gets large, not which one is faster for every single input size.

  4. Do I need to calculate Big O for every function I write?

    Not every function, but you should build the habit of thinking through it for any code that processes lists, strings, or other growing data, especially in loops or recursive calls.

  5. Why do interviewers care so much about Big O?

    Because it shows whether you understand the actual behavior of your solution, not just whether it produces the right output on a small test case. It is one of the fastest ways to tell if a candidate can reason about code, not just write it.

  6. Can space complexity matter more than time complexity?

    Yes, in some cases. An algorithm that runs fast but uses too much memory can crash on large inputs before a slower, more memory-efficient algorithm would even struggle.

Big O vs Best, Average, and Worst Case

Before wrapping up, it is worth untangling one more idea, since it trips up a lot of beginners: Big O is not the same thing as “worst case,” even though the two get used together so often that people start to think they are one and the same.

  • Best case: how the algorithm behaves on the most favorable input. Searching a list where the target happens to be the first item is a best case.
  • Worst case: how the algorithm behaves on the least favorable input. Searching a list where the target is the last item, or is not there at all, is a worst case.
  • Average case: the expected behavior across a typical range of inputs, not just the extremes.
  • Big O: a notation for describing how growth scales. It can be used to describe the best case, the worst case, or the average case. It is not, by itself, a synonym for “worst case.”

In everyday use, when someone says “this function is O(n),” they usually mean its worst-case behavior is O(n), because worst case is the most useful number to plan around. That convention is common enough that it rarely causes confusion in practice. But it helps to know that this is a convention, not a strict rule, since some algorithms have very different best-case and worst-case complexities. Binary search, for example, has a best case of O(1) (the target happens to be the exact middle element) and a worst case of O(log n). The next article in this series covers this distinction in more depth.

Conclusion

Big O notation gives you a way to reason about your code before it becomes a problem in production. It is not about counting exact operations. It is about understanding the shape of growth: does your code stay flat, grow steadily, or explode as input size increases?

The habit worth building from here is simple: whenever you write a loop or a recursive function, pause and ask what n is, and how the work scales with it. That single habit will make you a noticeably stronger Python developer and a much stronger interview candidate.

If you want to see how this fits into the bigger picture, the complete DSA with Python guide lays out the full learning path from here.

From here, the next article in this series looks at best-case, average-case, and worst-case time complexity, and why the exact same algorithm can behave very differently depending on which input it happens to receive.

External Resources

  1. Python Documentation — Time Complexity of Built-in Types
    A very useful reference when you start analyzing actual Python code. It lists the complexity of operations such as list access, append(), searching with in, sorting, slicing, and more.
    Python Time Complexity Documentation
  2. MIT OpenCourseWare — Understanding Program Efficiency
    This is a university-level resource from MIT that introduces algorithmic complexity, Big O notation, and different complexity classes. It is particularly relevant because the course itself teaches programming in Python.
    MIT OpenCourseWare: Understanding Program Efficiency

About the author Emmimal P Alexander is an AI Engineer and author dedicated to building applied AI systems that perform reliably in real-world production environments. She founded EmiTechLogic, an educational platform where she writes about neural networks, deep learning, large language models, and agentic AI—with a strong emphasis on practical engineering over idealized theory. Her background includes a BCA in computer applications, an MBA in business administration, and years of independent research and hands-on development in AI. She specializes in bridging theory to production-grade systems, tackling challenges like reliability, accountability, governance, AI hallucination, and well-defined decision boundaries. Emmimal is the author of Neural Networks and Deep Learning with Python: A Practical Approach and Agentic AI for Executives, where she explores not just models but the full systems around them—including failure modes and organizational risks. Through her writing, she prioritizes clarity over hype and sustainable impact over demos, helping practitioners and leaders understand how AI truly behaves when deployed. Check out her books on Amazon: https://www.amazon.com/stores/Emmimal-P-Alexander/author/B0DNGBTBRX

Leave a Reply

Your email address will not be published. Required fields are marked *

  • Rating