Skip to Content
Course content

234: Big-O Notation with Java Examples

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

A few years ago, I was reviewing a pull request from a junior developer who had built a feature to detect duplicate email addresses in a user sign-up list. In the staging environment, with about 200 test users, the code ran in a blink. It looked perfect. But the moment we pushed it to production—where we had 150,000 users—the server basically choked. The request timed out, the CPU spiked to 100%, and the logs were screaming. The culprit was a nested for loop that compared every email to every other email. In his head, it was just "two loops," but in reality, he'd written an algorithm that performed roughly 22 billion operations. That's when I sat him down to talk about Big-O notation.

The Language of Growth Rates

When we talk about Big-O, we aren't talking about how many milliseconds a piece of code takes to run. Your MacBook Pro will run a loop faster than a ten-year-old server, but the efficiency of the algorithm remains the same. Big-O is about how the runtime grows as the input size (which we call n) increases. It’s a way of describing the worst-case scenario so you don't get a nasty surprise when your user base grows.

Think of it as a trajectory. If you have 10 items, an inefficient algorithm might be fine. If you have a million, that same algorithm could take years to complete. By labeling code as $O(1)$, $O(n)$, or $O(n^2)$, we can predict exactly when a solution is going to break before we ever deploy it.

Measuring Time with Java Code

Let's look at this through some actual Java examples. The gold standard is $O(1)$, or Constant Time. This means no matter how huge your data set is, the operation always takes the same amount of time. Accessing an element in an array by its index is the classic example:

int[] scores = {92, 85, 77, 98, 60};
int topScore = scores[0]; // This is O(1)

Whether the array has 5 elements or 5 million, jumping to index 0 takes exactly one step. Now, most of the logic you'll write will be $O(n)$, or Linear Time. This happens when the time spent grows in direct proportion to the input. A simple search through an ArrayList is $O(n)$:

public boolean containsValue(List<String> list, String target) {
    for (String item : list) {
        if (item.equals(target)) return true;
    }
    return false;
}

If the list has 10 items, you might check 10 times. If it has a million, you might check a million times. It's a straight line on a graph. Then we hit the danger zone: $O(n^2)$, or Quadratic Time. This usually happens when you nest loops. This was the "production killer" I mentioned earlier:

public void findDuplicates(List<String> emails) {
    for (int i = 0; i < emails.size(); i++) {
        for (int j = i + 1; j < emails.size(); j++) {
            if (emails.get(i).equals(emails.get(j))) {
                System.out.println("Duplicate found: " + emails.get(i));
            }
        }
    }
}

For every single email, you are iterating through the entire list again. If you have 1,000 emails, you're doing 1,000,000 comparisons. This is where performance falls off a cliff.

Choosing the Right Tool for the Job

The secret to being a senior engineer isn't just knowing how to write code that works, but knowing which data structure turns an $O(n^2)$ problem into an $O(n)$ problem. In the duplicate email example above, we can swap the nested loop for a HashSet. Since checking if an item exists in a HashSet is $O(1)$, the entire process becomes $O(n)$.

I'll leave you with a quick mental cheat sheet: if you see a single loop, think $O(n)$. If you see nested loops over the same collection, alarm bells should go off for $O(n^2)$. If you see a search space being cut in half every time (like a binary search on a sorted array), you're looking at $O(\log n)$, which is incredibly efficient. Always ask yourself: "What happens to this method if the input size grows by 1,000x?" If the answer is "the server crashes," it's time to rethink your Big-O.




📋 Practical Task

Exercise: Optimizing a Duplicate User Detector

You have been handed a legacy method used to find if a list of usernames contains any duplicates. The current implementation uses nested loops and is causing performance issues in production. Your task is to rewrite the hasDuplicates method to improve its time complexity from $O(n^2)$ to $O(n)$.

import java.util.*;

public class UserRegistry {
    // CURRENT IMPLEMENTATION: O(n^2)
    public boolean hasDuplicates(List<String> usernames) {
        for (int i = 0; i < usernames.size(); i++) {
            for (int j = i + 1; j < usernames.size(); j++) {
                if (usernames.get(i).equals(usernames.get(j))) {
                    return true; 
                }
            }
        }
        return false;
    }

    public static void main(String[] args) {
        UserRegistry registry = new UserRegistry();
        List<String> users = Arrays.asList("alice", "bob", "charlie", "alice");
        System.out.println("Duplicates found: " + registry.hasDuplicates(users));
    }
}

Requirements:

  • Rewrite the hasDuplicates method using a HashSet to track usernames you have already seen.
  • The method must return true as soon as the first duplicate is encountered.
  • Ensure the time complexity of your new solution is $O(n)$.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.