Hello! Welcome back to our course on Data Structures and Algorithms.
In our last lesson, we introduced the fundamental concepts of time complexity and Big O notation. We learned how to categorize algorithms into classes like , , , and and understood the importance of focusing on the worst-case scenario and ignoring constants and lower-order terms.
Today, we'll build directly on that foundation. Our goal is to analyze the time complexity of iterative algorithms with loops. You'll learn a systematic method to break down different loop structures—from simple for loops to more complex nested patterns—and determine their Big O complexity. This skill is not just theoretical; it's a practical necessity for almost every coding interview problem you'll face.
1. A Systematic Approach to Analyzing Loops
When you see a piece of code with loops, the goal is to count the total number of "basic operations" it performs in the worst case. A simple and effective strategy is:
- Count the Iterations: Determine how many times the loop will run as a function of the input size, .
- Analyze the Loop Body: Determine the time complexity of the code inside the loop for a single iteration. This is often if it involves simple arithmetic, assignments, or comparisons.
- Multiply: The total time complexity is the (Number of Iterations) × (Complexity of the Loop Body).
Let's start by looking at some common loop patterns you'll encounter.
2. Single Loop Patterns
Linear Time:
This is the most common pattern. A loop's complexity is linear if its iteration count is directly proportional to the input size, . This typically happens when the loop variable is incremented or decremented by a constant value in each step.
Consider this Python code:
# Assume 'data' is a list of size n
def process_data(data):
# This loop runs n times
for item in data:
# Assume this is an O(1) operation
print(item)
Here, the loop runs len(data) times, which is . The operation inside, print(item), takes constant time for each item.
Therefore, the total complexity is .
To see a practical demonstration of how linear time scales, let's watch a short video segment.
Big O Notation — with Examples in Python
This video, from a segment we saw last time, clearly shows how an algorithm with O(n) complexity (finding the max element in a list) has a runtime that scales linearly with the input size.
Watch the segment from 05:40 to 08:12. Notice the straight line on the graph, which is the visual signature of linear growth.
Logarithmic Time:
We touched on last time with binary search. This highly efficient complexity often appears in loops where the control variable is multiplied or divided by a constant factor in each iteration. This pattern effectively shrinks the problem size exponentially.
Consider this loop:
def logarithmic_loop(n):
i = 1
while i < n:
# O(1) operation
print(i)
i *= 2 # i is doubled in each iteration
How many times does this loop run?
- Iteration 1:
i = 1 - Iteration 2:
i = 2 - Iteration 3:
i = 4 - Iteration 4:
i = 8 - ...
- Iteration k:
i = 2^(k-1)
The loop stops when . Let's say it runs times. At the end, will be roughly equal to . So, we have the equation . To solve for (the number of iterations), we take the logarithm of both sides: .
Since Big O notation ignores the base of the logarithm (it's just a constant factor), the complexity is .
For a detailed walkthrough of this analysis, the following video is excellent.
This video breaks down the process of counting iterations for a logarithmic loop. The methodical approach of setting up the equation and solving for the number of iterations is a very useful technique.
Watch from 08:24 to 11:22. Pay attention to how the presenter uses test values (like n=8 and n=9) to confirm the formula log₂(n) + 1 and how it simplifies to O(log n).
3. Nested Loop Patterns
Nested loops are another very common structure, where one loop is placed inside another.

Quadratic Time:
This complexity often arises from a simple nested loop where both loops iterate roughly times.
def find_pairs(data):
n = len(data)
# Outer loop runs n times
for i in range(n):
# Inner loop also runs n times
for j in range(n):
# O(1) operation
print(data[i], data[j])
Here, the outer loop runs times. For each of those iterations, the inner loop also runs times.
- Total Operations = (Outer Loop Iterations) × (Inner Loop Iterations) × (Loop Body Complexity)
- Total Operations =
The following video again provides a great visualization of how quickly runtimes grow compared to linear ones.
Big O Notation — with Examples in Python
This segment demonstrates O(n²) complexity using a Python function with nested loops. The graph at the end clearly shows the curve associated with quadratic growth, which is much steeper than the linear one we saw earlier.
Watch from 08:12 to 10:56. This will reinforce your understanding of why nested loops often lead to quadratic complexity.
Dependent Nested Loops: Still !
A slightly trickier case is when the inner loop's iterations depend on the outer loop's variable.
def find_unique_pairs(data):
n = len(data)
# Outer loop runs n times
for i in range(n):
# Inner loop's iterations depend on 'i'
for j in range(i + 1, n):
# O(1) operation
print(data[i], data[j])
Let's count the inner loop's iterations:
- When
i = 0, inner loop runs times. - When
i = 1, inner loop runs times. - ...
- When
i = n-2, inner loop runs 1 time. - When
i = n-1, inner loop runs 0 times.
The total number of operations is the sum of an arithmetic series: . This sum is equal to .
Following the rules of Big O, we drop the constants () and the lower-order term (), which leaves us with .
The next video gives a fantastic, methodical explanation of how to analyze this exact pattern.
This video walks through analyzing a dependent nested loop. The use of a table to track the variables and count the operations for each step is a powerful way to make the analysis concrete.
Watch from 04:48 to 08:24. Focus on how the number of inner loop iterations forms a summation and how that summation simplifies to a quadratic expression.
4. Combining Loops
What if loops aren't nested, but appear one after another?
Consecutive Loops:
When you have two separate, non-nested loops, you simply add their complexities.
def process_two_lists(list_m, list_n):
# This loop runs m times, where m = len(list_m)
for item in list_m:
print(item) # O(1)
# This loop runs n times, where n = len(list_n)
for item in list_n:
print(item) # O(1)
The total complexity is . If and are related (e.g., ), this simplifies to , which we know is just .
To reinforce this, please read the following brief section.
How to Analyse Loops for Complexity Analysis of Algorithms
This article explains how to handle consecutive loops. It confirms the simple rule: you add the complexities.
Read the section titled 'How to combine the time complexities of consecutive loops?'. The Python code example at the end is directly applicable.
Test your understanding!
Consider a function with three consecutive, non-nested loops. The first loop runs times. The second loop is a nested loop that runs times. The third loop runs times. What is the overall Big O complexity of the function?
Show answer
The total complexity is . When we add complexities, we only keep the dominant, highest-order term. In this case, grows much faster than . Therefore, the overall complexity simplifies to .
5. Practice: Your Turn to Analyze
Now, let's put this into practice. Analyze the time complexity of the following Python functions. Frame your answer in terms of , where is the size of the input list.
Problem 1:
def mystery_function_1(numbers):
n = len(numbers)
for i in range(n // 2):
print(numbers[i])
Show Answer
The time complexity is .
The loop runs from 0 to n // 2, which means it executes approximately times. In Big O notation, we drop the constant factor of . Since the work inside the loop is , the total complexity is .
Problem 2:
def mystery_function_2(n):
count = 0
i = n
while i > 1:
i = i // 3 # Integer division
count += 1
return count
Show Answer
The time complexity is .
The loop variable i is divided by a constant (3) in each iteration. This is the classic signature of a logarithmic time complexity. The loop will run approximately times. We ignore the base of the logarithm, so the complexity is .
Problem 3:
def mystery_function_3(numbers):
n = len(numbers)
for i in range(n):
for j in range(n):
for k in range(100): # A constant number of iterations
if numbers[i] == numbers[j]:
return True
return False
Show Answer
The time complexity is .
This function has three nested loops. The outer loop runs times. The second loop also runs times. The innermost loop runs a constant 100 times.
The total number of operations is approximately . According to the rules of Big O, we drop the constant factor of 100. This leaves us with .
Conclusion
Great work! Today you've learned a structured approach to analyzing the time complexity of iterative code, a skill that is tested in almost every technical interview.
Here are the key patterns to remember:
- Single loop with constant step:
- Single loop with multiplicative/divisive step:
- Two simple nested loops:
- Two dependent nested loops (triangular iteration):
- Consecutive (sequential) loops: Add the complexities, e.g., , and keep the dominant term.
So far, we've focused entirely on time complexity. However, an algorithm's efficiency is also measured by how much memory it uses. In our next lesson, we will introduce space complexity, exploring how to analyze an algorithm's memory footprint and why it's a critical consideration, especially in data engineering where datasets can be massive.