Hello! Welcome to the first lesson of your course.
Given your extensive background in software development, you've likely developed a strong intuition for code performance. This course aims to formalize that intuition, providing you with the rigorous theoretical framework used in top-tier technical interviews.
We'll begin with the absolute foundation of algorithm analysis. This lesson introduces asymptotic notation, the mathematical language we use to describe how an algorithm's performance scales. By the end of this lesson, you will be able to analyze the time and space complexity of both iterative and recursive algorithms using Big O, Omega, and Theta notation. This is the first and most critical step in evaluating and comparing different solutions to a problem.
1. Why We Need Asymptotic Analysis
When we analyze an algorithm, we don't care about the exact number of seconds it takes to run. That number depends on the programming language, the hardware, the operating system, and other external factors. Instead, we want to understand how the algorithm's resource usage (like time or memory) changes as the size of the input, which we'll call , gets larger.
Asymptotic analysis allows us to do this by focusing on the "growth rate" of the function that describes the algorithm's performance. We simplify the analysis by ignoring constant factors and lower-order terms, because as becomes very large, these details become insignificant.
For a high-level introduction to this idea, please watch the beginning of the following video.
Asymptotic Notations 101: Big O, Big Omega, & Theta (Asymptotic Analysis Bootcamp)
This video from Back To Back SWE, 'Asymptotic Notations 101', introduces the motivation behind asymptotic analysis and why we focus on an algorithm's behavior with very large inputs.
Watch from the beginning until 02:40. Focus on understanding why we analyze algorithms based on their performance on 'asymptotically' large inputs.
2. The Three Core Notations: O, Ω, and Θ
Asymptotic analysis uses three primary notations to describe the bounds of an algorithm's performance.

a) Big O Notation (O): The Upper Bound
Big O is the most common notation you'll encounter. It describes an upper bound on the growth rate of a function. In simpler terms, it provides a worst-case scenario: an algorithm will perform at least as well as its Big O complexity.
Formal Definition:
A function is in if there exist positive constants and such that for all , the following condition holds:
Here:
- is the function representing your algorithm's actual runtime.
- is a simpler function that provides an upper bound (e.g., , ).
- is the input size after which the upper bound holds true.
- is a constant factor that allows us to scale to make the inequality true.
The key insight is that we can always find some constant c that makes the bound work for sufficiently large n. This is why we can ignore constants (e.g., is the same as ) and lower-order terms (e.g., simplifies to ).
b) Big Omega Notation (Ω): The Lower Bound
Big Omega describes a lower bound. It provides a best-case scenario: an algorithm will take at least this long to run.
Formal Definition:
A function is in if there exist positive constants and such that for all :
c) Big Theta Notation (Θ): The Tight Bound
Big Theta describes a tight bound. This is the most precise description, as it means the algorithm's growth rate is bounded both from above and below by the same function.
Formal Definition:
A function is in if it is in both and . This means there exist positive constants , , and such that for all :
To solidify your understanding of these three notations with clear, visual examples and mathematical rigor, the next segment of the video is highly recommended.
Asymptotic Notations 101: Big O, Big Omega, & Theta (Asymptotic Analysis Bootcamp)
Continuing with the 'Asymptotic Notations 101' video, this part breaks down the mathematical definitions of Big O, Big Omega, and Big Theta with excellent graphical examples.
Watch from 02:40 to 21:21. Pay close attention to how the constants 'c' and 'n_0' are used to prove a bound, and the difference between a 'loose' and a 'tight' bound.
For an additional, text-based reference on the formal definitions, you can review the following resource.
Lecture 4: Formal Big-O, Omega and Theta
These slides from a University of Washington CSE course, titled 'Lecture 4: Formal Big-O, Omega and Theta', provide concise, formal definitions and proofs.
Review slides 8, 17, and 18 (you can find them by looking for the headers 'Definition: Big-O', 'Big-Ω [Omega]', and 'O, and Omega, and Theta [oh my?]'). These slides present the formal definitions we just discussed.
Test your understanding!
Consider an algorithm with a runtime function .
- Is this algorithm's runtime in ?
- Is its runtime in ?
- What is its tightest bound, expressed using notation?
Show answer
- Yes, it is in . Big O is just an upper bound. Since grows faster than , it certainly serves as an upper bound. It's a loose upper bound, but a valid one.
- Yes, it is in . Big Omega is a lower bound. Since grows faster than , the algorithm is guaranteed to be at least as slow as linear time for large . This is also a loose lower bound.
- The tightest bound is . The dominant term in is . Dropping the constant, we get . The function is both upper-bounded by and lower-bounded by , so its tight bound is .
3. Analyzing Algorithm Complexity in Practice
Now let's apply this theory to real code. We will analyze the complexity of both iterative and recursive algorithms.
Analyzing Iterative Algorithms
For iterative code, we typically count the number of operations performed in relation to the input size n.
O(1)(Constant Time): The number of operations does not depend on the input size. Examples include accessing an array element by index or performing a single arithmetic operation.def get_first_element(items): return items[0] # One operation, regardless of len(items)O(n)(Linear Time): The number of operations grows linearly with the input size. A common example is a single loop iterating through allnelements of a list.def find_max(items): max_val = items[0] for item in items: # Loop runs n times if item > max_val: max_val = item return max_valO(n^2)(Quadratic Time): The number of operations is proportional to the square of the input size. This often occurs with nested loops where both loops iterate up ton.def find_duplicates(items): for i in range(len(items)): # Outer loop runs n times for j in range(len(items)): # Inner loop runs n times if i != j and items[i] == items[j]: return True # n*n = n^2 operations in the worst case return FalseO(log n)(Logarithmic Time): The number of operations grows very slowly asnincreases. These algorithms typically reduce the problem size by a constant factor (e.g., by half) at each step. Binary search is the classic example.
The following video provides a practical, interview-focused walkthrough of these common complexities and more.
Big-O Notation - For Coding Interviews
This video from NeetCode, 'Big-O Notation - For Coding Interviews', is an excellent practical guide. It walks through code examples for the most common complexities you'll see in interviews.
Watch the segments covering O(1) (00:44-02:53), O(N) (02:53-06:37), O(N^2) (06:37-09:23), O(log N) (10:49-13:32), O(N log N) (13:32-15:02), and O(2^N) (15:02-17:23). Focus on how the structure of the code (loops, recursion) maps to its complexity.
For further reading and more code examples, you can consult this guide.
Complete guide to big-O notation and complexity analysis
The article 'Complete guide to big-O notation and complexity analysis' provides more code snippets and explanations for how to derive the complexity of different programming structures.
Read section '1.1.2 Example code analysis' for a step-by-step breakdown of various code examples. Then, skim section '1.1.3 How big-O classes compare to one another' to see a visual and tabular comparison of the growth rates.
Analyzing Recursive Algorithms
Analyzing recursion can be trickier. We need to define a recurrence relation that describes the runtime. A recurrence relation for a function defines it in terms of its value on smaller inputs.
For a recursive algorithm, the time complexity is typically the sum of:
- The time it takes to do the work inside the current function call (e.g., comparisons, arithmetic).
- The time it takes for all the recursive calls it makes.
Let's take a factorial function as an example:

As you can see, the recurrence is T(n) = T(n-1) + 3. This is because each call does a constant amount of work (the check, multiplication, and return) and then calls itself on an input of size n-1. Solving this recurrence shows that the complexity is O(n).
A more complex example is the classic recursive Fibonacci function:
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2) # Two recursive calls
The recurrence relation for this is . Because each call branches into two more calls, this leads to an exponential number of operations, resulting in a time complexity of . We will explore how to solve these recurrences more formally in a future lesson.
Test your understanding!
What is the time complexity of the following Python function?
def my_function(n):
i = 1
count = 0
while i < n:
i = i * 2
count += 1
return count
Show answer
The time complexity is .
The variable i doubles in each iteration of the while loop. The loop continues as long as i < n. Let's see the values of i in each iteration: 1, 2, 4, 8, 16, ..., . The loop stops when . To find the number of iterations k, we can solve for k:
Since the loop runs approximately times, the time complexity is . The base of the logarithm doesn't matter in Big O notation, as , and is just a constant factor.
Key Takeaways
- Asymptotic analysis describes how an algorithm's performance scales with input size
n, ignoring constants and lower-order terms. - Big O () provides an upper bound (worst-case).
- Big Omega () provides a lower bound (best-case).
- Big Theta () provides a tight bound (exact growth rate).
- For iterative code, complexity is found by analyzing loops. A single loop is often , nested loops can be , and algorithms that halve the input are often .
- For recursive code, complexity is described by a recurrence relation, which depends on the number of recursive calls and the work done at each step.
Next Steps
Excellent work on completing this foundational lesson. You now have the essential vocabulary and analytical tools to discuss algorithm efficiency.
In the next lesson, we will build directly on this by learning to differentiate between best, average, and worst-case complexities. We will see how a single algorithm can have different Big O complexities depending on the structure of its input data.