Recommended resources
The freely available resources this lesson will direct you to.
Welcome back! In our last lesson, we laid the groundwork for algorithmic reasoning by tracing code execution and identifying loop invariants. This helped us understand what an algorithm does and build confidence in its correctness. Now, we'll shift our focus from correctness to performance, answering the crucial question: "How efficient is this algorithm?"
This lesson introduces one of the most fundamental concepts in computer science: Big O notation. You will learn how to determine the time complexity of algorithms, specifically those involving sequential and nested loops. Instead of just timing code, which can be misleading, we'll develop a systematic way to count an algorithm's operations and describe how its runtime scales as the input size grows. This skill is the first step towards analyzing performance bottlenecks and is absolutely essential for technical interviews.
From Counting Steps to Measuring Growth
In the previous lesson, we manually traced algorithms, step by step. Big O notation formalizes this by giving us a language to talk about the growth rate of those steps. We're not interested in the exact number of operations for a small input, but rather the overall trend as the input size, which we'll call , becomes very large.
To begin, let's explore why simply timing our code isn't a reliable way to measure performance and why counting operations is a much more robust approach.
Complete Beginner's Guide to Big O Notation
Please watch this segment from the "Complete Beginner's Guide to Big O Notation" video by Colt Steele. It does an excellent job of motivating the need for Big O and introducing the core idea of counting operations instead of measuring time.
Watch from the beginning up to the introduction of counting operations. Pay close attention to the discussion of why using timers is problematic and how counting operations provides a more stable way to compare algorithms. The video covers: The motivation for Big O. An example of two functions with different performance characteristics. The problems with timing code and the shift to counting operations.
As the video explains, Big O notation is a "fuzzy" way of counting that focuses on the big picture. To achieve this, we follow a few simple but powerful rules:
- Worst-Case Analysis: We typically analyze the worst-case scenario. For example, if we're searching for an item in an array, the worst case is that the item is at the very end or not in the array at all.
- Drop Constants: Big O ignores constant factors. An algorithm that takes steps and one that takes steps are both considered . The growth is still linear.
- Drop Lower-Order Terms: We only care about the term that grows the fastest. If an algorithm takes steps, the term will dominate as gets large, so we simplify this to .
The following image provides a great visual for how these different growth rates compare. Notice how quickly the curve shoots up compared to the others.

Analyzing Loop Structures
With these rules in mind, let's learn how to apply them to the most common structures you'll encounter: sequential and nested loops. This is where you'll put theory into practice.
The article "How to find time complexity of an algorithm?" provides a clear, step-by-step method for doing this.
How to find time complexity of an algorithm? | Adrian Mejia Blog
This article breaks down complexity analysis into manageable rules for different code structures. We'll focus on the rules for sequential statements, loops, and nested loops.
Please read the following sections from the article: Start with Sequential Statements. This establishes our baseline: a simple operation without a loop takes constant time, or O(1). Next, read the subsection Linear Time Loops. This explains the fundamental rule for a single loop: multiply the number of iterations by the complexity of the work inside the loop. Finally, read the section on Nested loops statements. This is the core of today's lesson, showing how nested loops lead to multiplicative complexity.
Let's summarize and formalize what you've just read.
1. Sequential Loops
When you have two or more separate, non-nested loops, you add their complexities.
Consider this JavaScript code:
function processData(arr1, arr2) {
// First loop
for (const item of arr1) { // This loop runs n times, where n = arr1.length
console.log(item); // O(1) operation
}
// Second loop
for (const item of arr2) { // This loop runs m times, where m = arr2.length
console.log(item); // O(1) operation
}
}
The first loop has a complexity of . The second loop has a complexity of . The total complexity is . If both arrays had the same size , the complexity would be , which simplifies to just .
The "Learn Big O Notation In 12 Minutes" video provides a concise explanation of this.
Learn Big O Notation In 12 Minutes
This short clip clearly illustrates how to analyze two sequential loops.
Please watch the segment from this example where the presenter adds a second loop and explains why the complexity becomes O(n+a).
2. Nested Loops
When one loop is inside another, you multiply their complexities. This is the most common source of performance issues in brute-force algorithms.
Consider this example that finds all pairs in an array:
function findAllPairs(arr) {
const n = arr.length;
for (let i = 0; i < n; i++) { // This outer loop runs n times
for (let j = 0; j < n; j++) { // This inner loop runs n times *for each* outer iteration
console.log(arr[i], arr[j]); // O(1) operation
}
}
}
The outer loop runs times. For each of those iterations, the inner loop also runs times. The total number of operations is . Therefore, the time complexity is .
The same video explains this multiplicative effect well.
Learn Big O Notation In 12 Minutes
This next segment demonstrates the analysis of nested loops and the simplification rules.
Continue watching from nested loops to see the n * a and n^2 examples. Then, watch the explanation of simplification, which reinforces the rules for dropping constants and lower-order terms.
A Practical Note for JavaScript Developers
As an experienced developer, you know that real-world code isn't just simple loops; it's full of function calls to built-in methods or libraries. When analyzing complexity, you must account for the cost of these functions. A function call inside a loop is not automatically .
The "33 JavaScript Concepts" article has an excellent, practical guide on this.
Algorithms & Big O - 33 JavaScript Concepts
This resource is tailored to JavaScript and highlights the complexities of common built-in methods, which is directly applicable to your work.
First, read the section How to Analyze Your Code. This provides a simple, 3-step process that summarizes our lesson. Next, study the table under JavaScript Built-in Methods Complexity. This is crucial. For example, using arr.shift() (an O(n) operation) inside a loop that runs n times results in an overall complexity of O(n^2), a common pitfall.
Your Turn: An Exercise
Let's test your understanding. Determine the time complexity for each of the following JavaScript functions. Use to represent the length of the input array arr.
Function 1:
function function1(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
for (let j = 0; j < arr.length; j++) {
console.log(j);
}
return sum;
}
Function 2:
function function2(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < 5; j++) {
console.log(arr[i], j);
}
}
}
Function 3:
function function3(arr) {
for (let i = 0; i < arr.length / 2; i++) {
const other_index = arr.length - 1 - i;
// Assume arr.includes() is O(n) in the worst case
if (arr.includes(arr[i] + arr[other_index])) {
console.log("Found a match!");
}
}
}
Click here to see the solutions and explanations.
Function 1:
- The first
forloop runsntimes. Its complexity is . - The second
forloop also runsntimes. Its complexity is also . - Since these are sequential loops, we add their complexities: .
- Finally, we drop the constant
2, so the final complexity is .
Function 2:
- This is a nested loop, so you might immediately think . However, look closely at the inner loop's condition:
j < 5. - The outer loop runs
ntimes. - The inner loop runs a constant 5 times for each outer iteration.
- The total number of operations is .
- The complexity is . After dropping the constant
5, we get . This is a very important pattern to recognize!
Function 3:
- The
forloop runs fromi = 0ton / 2. This is still proportional ton, so the loop itself contributes . - Inside the loop,
arr.includes()is called. As noted in the33 JavaScript Conceptsresource,includes()is a linear search operation with a worst-case complexity of . - Since we have an operation inside a loop that runs times, we multiply the complexities: .
- Dropping the constant
1/2gives us a final complexity of .
Conclusion
Today, you've taken a significant step in your algorithmic journey. You've moved beyond simply making code work to analyzing how well it works. Understanding time complexity is not just an academic exercise; it's a practical tool for writing efficient code and a non-negotiable skill for technical interviews.
Key Takeaways:
- Big O Describes Growth: It's a high-level measure of how an algorithm's runtime scales with input size, focusing on the worst-case scenario.
- Simplify Ruthlessly: When calculating Big O, always drop constant factors and lower-order terms to focus on the dominant growth rate.
- Sequential Loops Add: The complexity of loops that run one after another is the sum of their individual complexities (e.g., is ).
- Nested Loops Multiply: The complexity of nested loops is the product of their complexities (e.g., is ).
- Know Your Tools: The complexity of built-in functions (like
Array.includes()orArray.shift()in JavaScript) contributes to the overall complexity of your algorithm.
In our next lesson, we will use this newfound knowledge to solve a common interview problem: given the constraints on an input size (e.g., "n can be up to 1,000,000"), how can you quickly determine which algorithmic approaches are feasible and which are too slow without even writing a line of code? This will further sharpen your ability to reason like an effective problem-solver.