Skip to Content
Course content

395: Big-O Notation Explained with Python Examples

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably heard people throw around terms like "O(n)" or "quadratic time" during code reviews or interviews. On the surface, Big-O notation feels like a math class requirement that doesn't belong in a code editor, but in reality, it's just a way for us to talk about how a piece of code will behave when the data gets huge. I don't care how fast your code runs with ten items in a list; I care about what happens when that list grows to ten million.

The "Just Loop Through It" Trap

Let's look at a common scenario: checking if a list contains any duplicate user IDs. When I first started out, my instinct was to just compare everything to everything else. It's the most intuitive way to think about the problem. Here is how that naive implementation looks in Python:

def has_duplicates_naive(user_ids):
    for i in range(len(user_ids)):
        for j in range(i + 1, len(user_ids)):
            if user_ids[i] == user_ids[j]:
                return True
    return False

This works perfectly fine for a handful of users. But notice what's happening here. For every single element in the list, we are scanning the rest of the list. If you have 10 IDs, you're doing roughly 100 checks. If you have 1,000 IDs, you're doing a million checks. This is what we call O(n²), or quadratic time. The "n" is the number of inputs, and the squared part means the workload grows exponentially relative to that input.

Why Quadratic Growth Kills Performance

The danger of O(n²) is that it's a silent killer. Your local tests will pass because your test data is small. But once this hits production and you're dealing with a real dataset, your CPU will spike and your API response times will crater. If your input size increases by 10x, a quadratic algorithm doesn't take 10x longer—it takes 100x longer. In a professional environment, that's the difference between a snappy app and a system outage.

Trading Memory for Speed with a Hash Set

Now, let's look at the "engineer's way" of solving this. Instead of re-scanning the list over and over, we can remember what we've already seen. In Python, the set is our best friend here because looking up an item in a set happens in "constant time," or O(1).

def has_duplicates_optimized(user_ids):
    seen = set()
    for uid in user_ids:
        if uid in seen:
            return True
        seen.add(uid)
    return False

In this version, we only loop through the list once. That's O(n), or linear time. If the list grows by 10x, the time it takes to run grows by 10x. That is a massive win. We've moved from a curve that shoots straight up into the air to a straight line that scales predictably.

The Space-Time Trade-off

Now, you might be wondering if there's a catch. There always is. In the first example, we used almost no extra memory; we just used the list we already had. In the optimized version, we created a set that could potentially grow to the size of the original list. We traded space complexity for time complexity.

As a rule of thumb, I almost always trade memory for speed. RAM is cheap; user patience is expensive. Understanding Big-O isn't about memorizing formulas; it's about recognizing these patterns so you can spot a performance bottleneck before it ever leaves your machine.




📋 Practical Task

Optimizing a High-Volume Common-Interest Finder

You've been handed a function that finds common interests between two users. The current implementation uses nested loops, and it's slowing down the "Suggested Friends" feature as the user base grows. Your task is to rewrite this function to move it from O(n*m) complexity to O(n+m) complexity.

def find_common_interests(user_a_interests, user_b_interests):
    # This is the slow, naive version
    common = []
    for interest in user_a_interests:
        for other_interest in user_b_interests:
            if interest == other_interest:
                common.append(interest)
    return common

# Test data
user_a = ["coding", "hiking", "reading", "gaming", "cooking"]
user_b = ["gaming", "cycling", "cooking", "swimming", "reading"]

print(find_common_interests(user_a, user_b)) 
# Expected output: ['reading', 'gaming', 'cooking'] (order may vary)

Requirement: Rewrite the find_common_interests function using a Python set to ensure that you only traverse each list once. Ensure the function still returns a list of the common interests.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.