Back to Data Structures & Algorithms
2025-12-248 min read

Big-O Notation (Data Structures & Algorithms)

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

Title: Big-O Notation (Data Structures & Algorithms) Using Python Examples

Why This Matters

Big-O notation is a crucial concept in computer science that helps us understand the efficiency of algorithms and data structures. It's essential for analyzing complexities, optimizing code, and solving real-world problems. Understanding Big-O notation can help you write more efficient code, save time, and solve problems faster during interviews or practical scenarios.

Prerequisites

Before diving into Big-O notation, it's essential to have a solid understanding of the following concepts:

  1. Basic Python syntax (variables, data types, operators)
  2. Control structures (if-else statements, loops - for and while)
  3. Data structures (lists, tuples, dictionaries)
  4. Functions and recursion
  5. Time complexity analysis
  6. Understanding basic sorting algorithms like bubble sort, selection sort, insertion sort, merge sort, quicksort, and heap sort.
  7. Familiarity with common data structures like arrays, linked lists, stacks, queues, trees, and graphs.

Core Concept

Big-O notation is an asymptotic notation used to describe the upper bound of an algorithm's time complexity or a function's growth rate. It provides a way to compare algorithms based on their efficiency, especially when the input size grows large. Big-O notation uses the big O (O) and little o (o) symbols to represent upper and lower bounds, respectively.

Big-O notation consists of three parts:

  1. Constant term: Represents the number of operations that are independent of the input size. It's usually dropped when comparing algorithms because it doesn't affect the growth rate as the input size increases.
  2. Linear term: Describes the leading term in the expression, which represents the main contribution to the time complexity.
  3. Higher order terms: Represents the remaining terms that contribute less to the overall complexity compared to the linear term. These terms are usually dropped when analyzing algorithms' efficiency.

Big-O notation uses the following symbols:

  • O(1): Constant time complexity (e.g., accessing an array element by index)
  • O(log n): Logarithmic time complexity (e.g., binary search)
  • O(n): Linear time complexity (e.g., linear search, traversal of a linked list)
  • O(n log n): Linearithmic time complexity (e.g., merge sort, quicksort)
  • O(n^2): Quadratic time complexity (e.g., bubble sort, naive matrix multiplication)
  • O(2^n): Exponential time complexity (e.g., brute force solutions for the traveling salesman problem)
  • O(n!): Factorial time complexity (e.g., some dynamic programming problems)

Worked Example

Let's analyze the time complexity of several Python functions:

Linear Search

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

In this example, we're searching for a specific value in an array. The time complexity of the linear search is O(n) because we need to iterate through each element in the array once.

Binary Search

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, we're using binary search to find a specific value in an array. The time complexity of the binary search is O(log n) because with each iteration, we halve the number of elements left to check.

Bubble Sort

def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]

The time complexity of bubble sort is O(n^2) because for each element, we compare it with all the subsequent elements. This results in n(n-1)/2 comparisons and swaps, which gives us a quadratic time complexity.

Selection Sort

def selection_sort(arr):
for i in range(len(arr)):
min_index = i
for j in range(i+1, len(arr)):
if arr[j] < arr[min_index]:
min_index = j
arr[i], arr[min_index] = arr[min_index], arr[i]

The time complexity of selection sort is O(n^2) because for each element, we find the minimum element in the remaining unsorted part of the array. This results in n comparisons and swaps, which gives us a quadratic time complexity.

Insertion Sort

def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key

The time complexity of insertion sort is O(n^2) in the worst-case scenario when the input array is sorted in reverse order. However, it's O(n) in the average and best cases when the input array is nearly sorted or already sorted.

Common Mistakes

  1. Neglecting constant factors: Constant factors are often ignored when comparing algorithms' efficiency. However, they can significantly impact performance in real-world scenarios.
  2. Incorrectly analyzing loops with multiple statements: When a loop contains multiple statements, it's essential to consider the number of operations performed during each iteration.
  3. Ignoring edge cases: Edge cases can lead to incorrect time complexity analysis. It's crucial to account for all possible scenarios when determining an algorithm's efficiency.
  4. Misunderstanding the big O notation symbols: Understanding the difference between upper and lower bounds, as well as common Big-O notation symbols, is essential for accurate analysis.
  5. Analyzing inefficient algorithms: Some algorithms may have a high time complexity but can still be used when the input size is small or the problem is simple enough to solve quickly.
  6. Neglecting space complexity: While this lesson focuses on time complexity, it's important to consider an algorithm's space complexity as well, especially in memory-constrained environments.

Practice Questions

  1. Analyze the time complexity of the following Python function that finds the maximum value in an array:
def find_max(arr):
max_value = arr[0]
for i in range(len(arr)):
if arr[i] > max_value:
max_value = arr[i]
return max_value
  1. Analyze the time complexity of the following Python function that finds the second largest value in an array:
def find_second_largest(arr):
first_max = None
second_max = None

for i in range(len(arr)):
if arr[i] > first_max:
second_max = first_max
first_max = arr[i]
elif (first_max is not None) and (arr[i] > second_max) and (arr[i] != first_max):
second_max = arr[i]

if second_max is None:
print("There is no second largest value.")
return None
else:
return second_max
  1. Analyze the time complexity of the following Python function that sorts an array using bubble sort:
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
  1. Analyze the time complexity of the following Python function that sorts an array using selection sort:
def selection_sort(arr):
for i in range(len(arr)):
min_index = i
for j in range(i+1, len(arr)):
if arr[j] < arr[min_index]:
min_index = j
arr[i], arr[min_index] = arr[min_index], arr[i]
  1. Analyze the time complexity of the following Python function that sorts an array using insertion sort:
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
  1. Analyze the time complexity of the following Python function that finds the kth smallest element in an unsorted array using quickselect:
def quickselect(arr, k):
if len(arr) == k:
return arr[k - 1]
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]

if k <= len(left):
return quickselect(left, k)
elif k <= len(arr):
return quickselect(right, k - len(left) - len(middle))
else:
return quickselect(right, k)
  1. Analyze the time complexity of the following Python function that finds the kth smallest element in an unsorted array using quickselect with a random pivot:
import random

def quickselect(arr, k):
if len(arr) == k:
return arr[k - 1]
pivot = arr[random.randint(0, len(arr) - 1)]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]

if k <= len(left):
return quickselect(left, k)
elif k <= len(arr):
return quickselect(right, k - len(left) - len(middle))
else:
return quickselect(right, k)
  1. Analyze the time complexity of the following Python function that finds the kth smallest element in an unsorted array using a modified version of quickselect with a median-of-three pivot:
def quickselect(arr, k):
if len(arr) == k:
return arr[k - 1]
middle = len(arr) // 2
left_pivot = arr[middle - 1]
right_pivot = arr[middle + 1]
if arr[middle] > right_pivot:
left_pivot = arr[0]
right_pivot = arr[len(arr) - 1]
elif arr[middle] < left_pivot:
left_pivot = arr[len(arr) - 1]
right_pivot = arr[0]
pivot = arr[(len(arr) - 1) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]

if k <= len(left):
return quickselect(left, k)
elif k <= len(arr):
return quickselect(right, k - len(left) - len(middle))
else:
return quickselect(right, k)

FAQ

  1. Why do we use Big-O notation to analyze algorithms' efficiency?
  • Big-O notation helps us understand the upper bound of an algorithm's time complexity or a function's growth rate, allowing us to compare algorithms based on their efficiency as the input size grows large.
  1. What is the difference between O(1), O(log n), O(n), and O(n log n) time complexities?
  • O(1): Constant time complexity (e.g., accessing an
Big-O Notation (Data Structures & Algorithms) | Data Structures & Algorithms | XQA Learn