Episode 1:Imagine two developers solving the same problem.
Both solutions return the correct answer. Both pass every test case. Both get deployed to production without any issues.
So how do you decide which one is actually better?
In software engineering, getting the correct output is only the first step. As applications grow and start handling larger amounts of data, the gap between two “correct” solutions can become enormous. One solution might continue working smoothly as traffic increases, while another might gradually slow down.
To make those decisions objectively, we need a way to measure our code. That is exactly what this episode is about.
When you’re just starting out, the first question you ask about any piece of code is:
“Does it work?”
And honestly, that’s the right place to begin.
If your solution doesn’t produce the correct result, nothing else matters. However, as you gain experience, you’ll often find yourself in situations where multiple solutions solve the same problem correctly.
That’s when a different question starts to matter:
“Which solution is more efficient?”
This is where Time Complexity and Space Complexity come into the picture.
What Are We Actually Measuring?When we talk about the complexity of an algorithm, we’re usually interested in two things.
The first is time complexity, which describes how the number of operations an algorithm performs grows as the input size increases. In simple terms, it helps us understand how much additional work the algorithm has to do when it receives more data.
The second is space complexity, which tells us how much additional memory the code needs as the input size grows.
One thing that often confuses beginners is that we are not measuring actual seconds or megabytes.
We are not asking how fast your laptop runs compared to mine. We are not comparing JavaScript against Java or Python. Those things can vary depending on hardware, operating systems, compilers, and many other factors.
Instead, we focus on something much more useful:
How does the solution scale as the input gets larger? What happens if the input doubles? What happens if it becomes ten times larger? What happens if it grows from a thousand items to a million?
That way of thinking is what makes complexity a reliable measuring tool regardless of the machine or language you are using.
Big O NotationBig O Notation is simply the language we use to describe that growth.
The “O” stands for “order of,” and the expression inside the brackets tells us how the algorithm grows relative to the size of the input. The input size is usually represented by the letter n.
At first glance, the notation may look mathematical and intimidating, but the underlying idea is surprisingly simple. Once you understand the common patterns, reading Big O becomes second nature.
The Most Common Complexities You’ll SeeO(1): Constant TimeAn algorithm is considered O(1) when the amount of work stays the same no matter how large the input becomes.
For example, imagine you have an array containing thousands or even millions of names, and you want to access the first item.
function getFirstItem(arr) {
return arr[0];
}Whether the array contains 10 items or 10 million items, retrieving the first element still requires the same operation.
The input size has no impact on the amount of work being done, which is why this is called constant time.
O(n): Linear TimeWith O(n), the amount of work grows in direct proportion to the size of the input.
Suppose you want to find a particular name inside an unsorted array.
function findName(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1;
}Since the array isn’t sorted and doesn’t provide any shortcuts, you may have to check each item one by one.
If there are 10 items, you might inspect up to 10 elements.
If there are 1,000 items, you might inspect up to 1,000 elements.
If there are a million items, you might inspect up to a million elements.
The work increases alongside the input size, so this is O(n), also known as linear time.
O(n²): Quadratic TimeQuadratic time usually appears when you have a loop running inside another loop.
function printAllPairs(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
console.log(arr[i], arr[j]);
}
}
}Here, every element is paired with every other element.
If the array contains 10 items, the inner operation runs roughly 100 times.
If the array contains 100 items, it runs roughly 10,000 times.
The growth accelerates quickly, which is why quadratic solutions often become problematic when working with large datasets.
Many performance bottlenecks can be traced back to an unnoticed O(n²) operation.
O(log n): Logarithmic TimeLogarithmic complexity often feels strange when you first encounter it, but the idea is actually very intuitive.
Instead of examining every item one by one, the algorithm repeatedly cuts the problem in half.
Think about searching for a word in a physical dictionary.
You wouldn’t start from page one and move forward page by page. Instead, you would open the dictionary somewhere near the middle, check whether your word comes before or after that section, eliminate half the remaining pages, and repeat the process.
Each step removes half of the remaining work.
That is the essence of O(log n).
Because the search space shrinks so aggressively, the number of steps grows very slowly. Even when dealing with extremely large datasets, logarithmic algorithms remain remarkably efficient.
We’ll see a classic example of this when we cover Binary Search later in the series.
O(n log n)This complexity appears frequently in efficient sorting algorithms.
It sits somewhere between O(n) and O(n²).
Algorithms with O(n log n) complexity usually combine two ideas:
- They process all elements.
- They repeatedly divide the problem into smaller pieces.
Popular sorting algorithms such as Merge Sort and Quick Sort often achieve this complexity.
As datasets become larger, O(n log n) performs dramatically better than O(n²), which is why it is considered the standard target for efficient sorting.
Visualizing the DifferenceIf we arranged these complexities from most efficient to least efficient as the input grows, the order would look like this:
O(1) → O(log n) → O(n) → O(n log n) → O(n²)
You can think of them like this:
- O(1) stays flat no matter how large the input becomes.
- O(log n) grows very slowly.
- O(n) grows steadily alongside the input.
- O(n log n) grows faster than linear time but remains practical.
- O(n²) rises sharply and becomes expensive very quickly.
This ranking is something you’ll use constantly as you learn algorithms and data structures.
Whenever you encounter a new solution, one of the first things you should ask is:
“Where does this algorithm sit on that scale?”
What About Space Complexity?Everything we’ve discussed so far has focused on time, meaning the amount of work being performed.
Space complexity follows the same idea, except we’re measuring memory usage instead of steps.
Consider this example:
function doubleAll(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] * 2);
}
return result;
}The new array grows alongside the input array. If the input doubles in size, the extra memory required also doubles.
Because the memory usage grows with the input, this is O(n) space.
Now compare that with:
function sumAll(arr) {
let total = 0;
for (let i = 0; i < arr.length; i++) {
total += arr[i];
}
return total;
}Here, we’re only using a single variable regardless of how large the input becomes.
Whether the array contains 10 elements or 10 million, the extra memory remains essentially the same.
That makes this O(1) space.
In real-world development, you’ll often encounter trade-offs between time and memory. Sometimes using extra memory can significantly reduce execution time. Other times, minimizing memory usage may require additional processing.
Understanding both time and space complexity helps you make those trade-offs consciously rather than accidentally.
One Practical Rule to RememberWhen calculating Big O, we focus only on the part of the algorithm that grows the fastest.
For example:
- O(n + 50) becomes O(n)
- O(3n) becomes O(n)
- O(n² + n) becomes O(n²)
Why?
Because Big O is concerned with long-term growth.
As the input becomes very large, constant values and smaller terms become insignificant compared to the dominant term.
The goal is not to count every operation perfectly.
The goal is to understand how the algorithm behaves as the input continues to grow.
The Habit That Will Change How You Read CodeFrom now on, whenever you look at a piece of code, train yourself to ask a simple question:
"As the input gets larger, what happens to the amount of work being done?"
If the work stays the same, you're probably looking at O(1).
If it grows alongside the input, it's likely O(n).
If you see a loop inside another loop, there's a good chance you're dealing with O(n²).
If the problem size keeps getting cut in half, you're probably looking at O(log n).
Developing this habit is one of the most valuable steps you can take as a programmer. It changes the way you read code, write code, and evaluate solutions.
In the next episode, we'll move into Arrays. Now that you understand how to measure performance, the operations we discuss will have much more meaning because you'll be able to analyze not just what they do, but also how efficiently they do it.
♦Before We Compare Solutions, We Need a Way to Measure Them was originally published in Code Like A Girl on Medium, where people are continuing the conversation by highlighting and responding to this story.