Back to Data Structures & Algorithms
2025-11-285 min read

Big-O Notation (O-notation) (Data Structures & Algorithms)

Learn Big-O Notation (O-notation) (Data Structures & Algorithms) step by step with clear examples and exercises.

Why This Matters

Big-O notation is a fundamental tool in computer science that helps us understand and compare the efficiency of algorithms and data structures. It's crucial for solving real-world problems, optimizing code, and making informed decisions during interviews or exams. Understanding Big-O notation can help you avoid common pitfalls in your code, such as writing inefficient solutions that consume excessive time and resources.

Big-O notation allows us to predict how the running time of an algorithm will scale as the size of the input increases. By choosing efficient algorithms, we can solve problems faster and use fewer resources, which is essential for building scalable and high-performance systems.

Prerequisites

Before diving into Big-O notation, it's essential to have a basic understanding of:

  1. Basic Python syntax
  2. Data structures like lists, tuples, and dictionaries
  3. Control flow statements (if-else, for loops, while loops)
  4. Functions and recursion
  5. Understanding the concepts of time complexity and space complexity

Core Concept

Big-O notation is an abstract way to describe the performance or complexity of an algorithm as its input size grows. It provides a simplified view of how long an algorithm takes to run or how much space it requires in terms of the number of operations, comparisons, or memory accesses required.

The Big-O notation uses uppercase O followed by a function that describes the growth rate of the algorithm. For example:

  1. O(1) - Constant time complexity: An operation takes a constant amount of time regardless of input size (e.g., accessing an element in an array using its index).
  2. O(n) - Linear time complexity: The running time increases linearly with the size of the input (e.g., searching for an item in an unsorted list).
  3. O(log n) - Logarithmic time complexity: The running time grows logarithmically with the size of the input (e.g., binary search in a sorted array).
  4. O(n^2) - Quadratic time complexity: The running time increases as the square of the size of the input (e.g., bubble sort, brute-force solutions to some problems).
  5. O(2^n) - Exponential time complexity: The running time doubles with each addition to the input (e.g., naive recursive solutions to Fibonacci or Tower of Hanoi problems).

Note that that Big-O notation is an approximation and doesn't account for constant factors, which can have a significant impact on performance in practice. However, it helps us compare different algorithms by focusing on the dominant terms in their time complexity expressions.

Worked Example

Let's consider a simple example of finding an item in an unsorted list using linear search and binary search, and compare their Big-O notations.

Linear Search (O(n))

def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i
return -1

In this example, the function iterates through each element in the array, which takes n operations. The worst-case time complexity is O(n).

Binary Search (O(log n))

def binary_search(arr, target):
low = 0
high = len(arr) - 1

while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1

return -1

In this example, the function performs a binary search on the array. In each iteration, it divides the search space in half, reducing the number of elements to check by a factor of two. The worst-case time complexity is O(log n).

Common Mistakes

  1. Misunderstanding Big-O notation: Some developers confuse Big-O notation with average or best-case scenarios instead of focusing on the worst-case scenario, which is more relevant when comparing algorithms.
  2. Overlooking constant factors: While Big-O notation abstracts away constant factors, they can still have a significant impact on performance in practice.
  3. Ignoring data distribution: The time complexity of some algorithms depends on the distribution of the input data. For example, binary search works efficiently when the array is sorted, but linear search may be faster for random data.
  4. Neglecting auxiliary space: Big-O notation can also describe the space complexity of an algorithm in terms of additional memory required. Be aware that both time and space complexities are important factors to consider.
  5. Assuming that a single operation has constant time complexity (O(1)): Some operations, such as accessing elements in dynamic arrays or linked lists, may have variable time complexity depending on the current size of the data structure.
  6. Incorrectly analyzing recursive functions: When analyzing recursive functions, it's essential to consider the number of recursive calls and their relationship to the input size. For example, some recursive solutions may have a time complexity that grows exponentially with the input size (O(2^n)) due to excessive recursion.

Practice Questions

  1. What is the time complexity of the following function, which finds the second largest number in a list using two passes:
def find_second_largest(arr):
max1 = float('-inf')
max2 = float('-inf')

for num in arr:
if num > max1:
max2, max1 = num, max1
elif num > max2 and num != max1:
max2 = num

return max2

Answer: O(n)

  1. What is the time complexity of the following function, which finds the maximum number in a list using recursion:
def find_max(arr):
if len(arr) == 1:
return arr[0]

mid = len(arr) // 2
if arr[mid] > arr[len(arr) - 1]:
return find_max(arr[:mid])
else:
return find_max(arr[mid:])

Answer: O(n log n) (recursive solution with a logarithmic number of recursive calls and linear time for each call)

FAQ

What does Big-O notation tell us about an algorithm's performance?

Big-O notation gives us a high-level understanding of how the running time of an algorithm will scale as the size of the input increases. It helps us compare different algorithms and make informed decisions when choosing solutions for specific problems.

Why is it important to consider worst-case scenarios in Big-O notation?

Focusing on worst-case scenarios allows us to determine the upper bound on the running time or space requirements of an algorithm, ensuring that our solutions perform well even under the most challenging conditions.

How can I find the Big-O notation for a given algorithm?

To find the Big-O notation for an algorithm, analyze its running time or space requirements and express them in terms of the input size (n). Identify any dominant terms and ignore constant factors and lower-order terms. For example:

  1. Linear search: O(n)
  2. Binary search: O(log n)
  3. Bubble sort: O(n^2)
  4. QuickSort: O(n log n) (average case) or O(n^2) (worst case)
  5. Merge Sort: O(n log n)
  6. Fibonacci recursion: O(2^n)
  7. Tower of Hanoi recursion: O(2^n)
Big-O Notation (O-notation) (Data Structures & Algorithms) | Data Structures & Algorithms | XQA Learn