Hello! Welcome to your first lesson in our course on Data Structures and Algorithms. As you begin your journey toward a data engineering role, a solid grasp of these concepts is essential for acing technical interviews.
Today, we'll start with the most fundamental topic in algorithm analysis: time complexity and Big O notation. By the end of this lesson, you'll be able to explain what these concepts mean and recognize the most common complexity classes. This is the language we'll use throughout the course to compare different algorithms and understand their efficiency.
1. Why Do We Need to Analyze Algorithms?
Imagine you have two different Python functions that accomplish the same task, like finding a specific value in a list. How do you decide which one is "better"?
You could time them using a stopwatch or Python's time module. However, this approach is unreliable. The result would depend on your computer's speed, what other programs are running, and the specific size of the list you tested with.
We need a more objective way to describe an algorithm's efficiency that is independent of the hardware or specific test data. This is where time complexity comes in. Instead of measuring time in seconds, we measure it by counting the number of basic operations an algorithm performs as its input size grows.
The input size, which we'll call , is a crucial part of this. We want to know how the algorithm's performance changes as gets very large. Does the runtime double? Does it quadruple? Or does it barely change at all? Big O notation is the standard way to express this relationship.
2. Introducing Big O Notation
Big O notation provides a high-level description of an algorithm's time or space complexity. It focuses on the "big picture"—how the number of operations grows with the input size , especially as becomes very large.
To get a feel for this, let's watch a short segment from a video by NeetCode, a popular resource for interview preparation.
Big-O Notation - For Coding Interviews
This video provides a concise, interview-focused introduction to Big O notation. Pay close attention to the core idea: analyzing how runtime scales as the input size grows and the concept of ignoring constants.
Watch the section from 00:44 to 01:51. The key takeaway is that Big O focuses on the relationship between input size (n) and runtime, not the exact time.
As the video explained, Big O simplifies our analysis by following two key rules:
- Drop Constants: An algorithm that takes operations and one that takes operations are both considered (pronounced "Big O of n"). As gets large, the constant multiplier becomes less significant than the fact that the runtime grows linearly with .
- Drop Lower-Order Terms: If an algorithm takes operations, we only care about the term that grows the fastest. For large , the term will dominate the others. Therefore, we simplify the complexity to .
To see this in action, the article "Complete guide to big-O notation and complexity analysis" has a great table illustrating why we can ignore lower-order terms.
Complete guide to big-O notation and complexity analysis
This section of the article clearly demonstrates how the dominant term in a complexity expression quickly overshadows the others as the input size 'n' increases.
Read the subsection titled '1.1.1 Simplifications'. Focus on the first table that compares n^2 with n^2 + 10n + 5.
3. The Big O Hierarchy: Common Complexity Classes
Algorithms are categorized into different "complexity classes." Understanding this hierarchy is crucial because it tells you at a glance how efficient an algorithm is.
This chart provides an excellent visual overview of the most common classes. The x-axis represents the input size (), and the y-axis represents the number of operations.

Now, let's explore the most important complexity classes with concrete examples. We'll use the NeetCode video again, as it provides clear, code-oriented explanations.
O(1) — Constant Time
An algorithm is if its execution time does not change as the input size increases. These are the most efficient algorithms.
Big-O Notation - For Coding Interviews
This segment explains constant time complexity, O(1). Notice how the operations mentioned don't depend on the size of the array or hash map.
Watch the section from 01:51 to 02:53.
Key examples:
- Accessing an element in a Python list by its index (e.g.,
my_list[3]). - Adding or removing an element from the end of a list (
my_list.append()ormy_list.pop()). - Looking up a value in a Python dictionary by its key (e.g.,
my_dict['name']).
O(n) — Linear Time
An algorithm is if its runtime grows in direct proportion to the input size . If you double the input size, the runtime also roughly doubles.
Big-O Notation - For Coding Interviews
Next, let's look at linear time, O(n). This is a very common complexity class, often involving loops that visit each element of a collection once.
Watch the section from 02:53 to 04:30. Focus on the examples of summing an array and looping through a list.
Key examples:
- Iterating through all elements in a list with a
forloop. - Finding a specific item in an unsorted list (in the worst case, you have to check every element).
- Printing every element in a list.
O(n²) — Quadratic Time
An algorithm is if its runtime is proportional to the square of the input size. These algorithms often involve nested loops. If you double the input size, the runtime roughly quadruples.
Big-O Notation - For Coding Interviews
Quadratic time, O(n²), often appears when you need to compare every element in a collection to every other element. Watch this segment for a clear explanation.
Watch from 06:37 to 08:00. The example of iterating over a 2D grid is a classic illustration of O(n²).
Key examples:
- A nested loop where both the outer and inner loops iterate times.
- Finding all pairs of elements in a list.
- Simple sorting algorithms like Bubble Sort (which we'll cover later).
Test your understanding!
You have a list of n numbers. You write an algorithm that finds the sum of every possible pair of numbers in the list. Is this algorithm's time complexity more likely to be or ? Why?
Show answer
The time complexity is . To find the sum of every possible pair, you would typically use a nested loop. The outer loop picks the first number of the pair, and the inner loop iterates through the rest of the numbers to form pairs with it. This "for each element, do something with every other element" pattern is a hallmark of complexity.
O(log n) — Logarithmic Time
Logarithmic time is one of the most powerful concepts in algorithms. An algorithm is if its runtime grows logarithmically with the input size. This means that even if you double the input size, the runtime only increases by a constant amount. These algorithms are extremely efficient for large inputs. They typically work by repeatedly halving the input.
Big-O Notation - For Coding Interviews
Logarithmic time, O(log n), is a game-changer for performance. This part of the video explains it using the classic example of binary search.
Watch the section from 10:49 to 13:18. The key intuition is that with each step, the algorithm discards half of the remaining data.
Key examples:
- Binary Search: Finding an element in a sorted array.
- Finding an element in a balanced Binary Search Tree (a data structure we'll study later).
To give you a sense of how powerful this is: to find one name among a billion sorted names, a linear search () would take, in the worst case, a billion operations. A binary search () would take only about 30!
4. Why the Difference Matters
The practical difference between these complexity classes is enormous, especially as you work with the large datasets common in data engineering.
Let's look at a table from the "Complete guide to big-O notation" article, which shows the time required for a computer that can perform a billion instructions per second to complete a task, given different complexities.
Complete guide to big-O notation and complexity analysis
This table powerfully illustrates the real-world implications of different Big O classes. It shows how quickly inefficient algorithms become impractical.
Find the section '1.1.3 How big-O classes compare to one another'. Study the graph and, more importantly, the table below it.
As you can see, an algorithm with complexity becomes slow for an input of 50,000, while one with or is unusable for even tiny inputs. This is why knowing how to choose an efficient algorithm is a critical skill for any software or data engineer.
5. Practice: Analyzing Simple Code
Now let's apply what you've learned. The best way to get comfortable with Big O is to practice analyzing code.
Problem 1: What is the time complexity of the following Python function?
def print_first_element(my_list):
if len(my_list) > 0:
print(my_list[0])
Show Answer
The time complexity is (constant time).
The function performs a fixed number of operations: it checks the length of the list and then accesses the first element. The time it takes to do this does not depend on whether the list has 10 elements or 10 million elements. Accessing an element by its index in a Python list is an operation.
Problem 2: What is the time complexity of this function?
def find_sum(numbers):
total = 0
for num in numbers:
total += num
return total
Show Answer
The time complexity is (linear time), where is the number of elements in the numbers list.
The function iterates through the list once. The number of operations (additions) is directly proportional to the size of the list. If the list has numbers, the loop will run times.
Problem 3: And what about this one?
def contains_duplicates(numbers):
for i in range(len(numbers)):
for j in range(len(numbers)):
if i != j and numbers[i] == numbers[j]:
return True
return False
Show Answer
The time complexity is (quadratic time), where is the number of elements in the numbers list.
This function has a nested loop. The outer loop runs times, and for each of those iterations, the inner loop also runs times. This results in approximately comparisons in the worst case.
Key Takeaways
- Time Complexity measures how an algorithm's runtime scales with the input size ().
- Big O Notation is the language we use to express this scaling, focusing on the worst-case scenario.
- When determining Big O, we ignore constants and lower-order terms because we care about the rate of growth for very large inputs.
- The hierarchy of common complexities, from most to least efficient, is:
< < < < < <
Understanding Big O is the first step to writing efficient and scalable code. It allows you to predict performance and make informed decisions when choosing between different ways to solve a problem.
In our next lesson, we will build directly on this foundation. We'll practice analyzing the time complexity of more complex iterative algorithms that involve different kinds of loops, which is a very common task in technical interviews.