Skip to content
IRC-Coding IRC-Coding
Big O Notation Runtime Complexity Algorithm Efficiency O(1) O(n) O(log n) Worst Case

Big O Notation Simply Explained

Big O notation describes growth behavior of runtime/memory with increasing input. With worst-case analysis

S

schutzgeist

2 min read
Big O Notation Simply Explained

Big O Notation – Runtime Complexity & Algorithm Efficiency

This article is a concept explanation about Big O notation – including exam questions and tags.

In a Nutshell

Big O notation describes how strongly the runtime or memory requirement of an algorithm grows in relation to the input size – it is a measure of efficiency.

Compact Technical Description

Big O notation is used to analyze the asymptotic complexity of an algorithm – it abstracts from concrete runtimes and focuses on behavior with growing input. In the worst case (“Worst Case”), the number of calculation steps or memory accesses required by an algorithm is specified. Examples: O(1) = constant, O(n) = linear, O(n²) = quadratic, O(log n) = logarithmic. This allows algorithms to be compared independently of hardware.

Exam-Relevant Key Points

  • Big O describes growth behavior of runtime/memory
  • “Worst Case” is considered by default
  • Typical notations: O(1), O(n), O(log n), O(n²), O(n log n)
  • Logarithmic e.g. in binary search (IHK-relevant)
  • Quadratic algorithms are inefficient with large data
  • Efficient algorithms save resources → economically relevant
  • Big O analysis must be documented in complex algorithms

Core Components

  1. Constant: O(1)
  2. Linear: O(n)
  3. Logarithmic: O(log n)
  4. Linear-logarithmic: O(n log n)
  5. Quadratic: O(n²)
  6. Cubic: O(n³)
  7. Exponential: O(2ⁿ)
  8. Worst-Case Analysis
  9. Best-Case, Average-Case
  10. Relevance for Scalability

Practical Example

// Comparison: linear vs. binary search
Linear Search: O(n)
Binary Search: O(log n), only for sorted lists

Explanation: Linear search traverses the list completely, binary search halves the search field with each step – significantly more efficient with large amounts of data.

Advantages and Disadvantages

Advantages

  • Comparability of algorithms independent of implementation
  • Detection of potential bottlenecks
  • Better decisions in design phases

Disadvantages

  • No statements about concrete runtimes on specific hardware
  • Only worst case without average values considered
  • Theoretical, not always directly transferable to real conditions

Typical Exam Questions (with Short Answers)

  1. Big O notation describes? Complexity of an algorithm in terms of runtime or memory requirement with growing input.
  2. What does O(1) mean? Runtime is constant – independent of input size.
  3. More efficient: O(n) or O(log n)? O(log n), as it is significantly faster with growing data volume.
  4. What does “n” stand for in O(n)? Number of input elements.
  5. Algorithm typically O(n log n)? Merge Sort or Quick Sort on average.
  6. Why is O(n²) problematic? Runtime grows exponentially with large data volumes.
  7. Simultaneously O(n) and O(n²)? No – the dominant component is always specified.
  8. Document Big O? Through code comments, diagrams or formal analysis.

Most Important Sources

  1. https://www.bigocheatsheet.com/
  2. https://visualgo.net/en
  3. https://www.geeksforgeeks.org/analysis-of-algorithms-set-1-asymptotic-analysis/
  4. https://cs50.harvard.edu/
  5. https://www.youtube.com/results?search_query=big+o+notation

In a Nutshell

Big O notation describes how an algorithm’s runtime or memory usage grows as the input size increases, focusing on the worst-case scenario.

What is Big O Notation?

Big O notation is a mathematical notation that describes the limiting behavior of a function when the argument tends towards a particular value or infinity. In computer science, it’s used to classify algorithms according to how their runtime or space requirements grow as the input size grows.

Why Big O Matters

  1. Performance Prediction: Estimate how algorithms scale
  2. Algorithm Selection: Choose the most efficient algorithm
  3. System Design: Plan for expected load
  4. Optimization: Identify performance bottlenecks

Common Big O Complexities

O(1) - Constant Time

Runtime doesn’t depend on input size.

def get_first_element(arr):
    return arr[0]  # Always one operation

def hash_table_lookup(dict, key):
    return dict[key]  # Average case: one operation

Examples: Array access, hash table lookup, stack push/pop

O(log n) - Logarithmic Time

Runtime grows logarithmically with input size.

def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return -1

Examples: Binary search, balanced tree operations, heap operations

O(n) - Linear Time

Runtime grows linearly with input size.

def linear_search(arr, target):
    for element in arr:
        if element == target:
            return True
    return False

def find_max(arr):
    max_val = arr[0]
    for element in arr:
        if element > max_val:
            max_val = element
    return max_val

Examples: Linear search, finding maximum/minimum, simple traversals

O(n log n) - Linearithmic Time

Combination of linear and logarithmic growth.

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    
    result = []
    i = j = 0
    
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    
    result.extend(left[i:])
    result.extend(right[j:])
    return result

Examples: Merge sort, heap sort, quick sort (average case)

O(n²) - Quadratic Time

Runtime grows quadratically with input size.

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]
    return arr

def find_duplicates(arr):
    duplicates = []
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] == arr[j]:
                duplicates.append(arr[i])
                break
    return duplicates

Examples: Bubble sort, selection sort, nested loops

O(2ⁿ) - Exponential Time

Runtime doubles with each additional input element.

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

def power_set(arr):
    if not arr:
        return [[]]
    
    first = arr[0]
    rest = power_set(arr[1:])
    
    with_first = [[first] + subset for subset in rest]
    return rest + with_first

Examples: Recursive Fibonacci, generating all subsets, brute force problems

Big O Complexity Comparison

Input SizeO(1)O(log n)O(n)O(n log n)O(n²)O(2ⁿ)
101310331001,024
1001710066410,0001.27×10³⁰
1,0001101,0009,9661M1.07×10³⁰¹
10,00011310,000132,877100M

Space Complexity

Space complexity measures memory usage, not runtime.

O(1) - Constant Space

def find_max_constant_space(arr):
    max_val = arr[0]
    for element in arr:
        if element > max_val:
            max_val = element
    return max_val  # Only uses one extra variable

O(n) - Linear Space

def reverse_array(arr):
    reversed_arr = []
    for element in arr:
        reversed_arr.insert(0, element)
    return reversed_arr  # Uses O(n) extra space

O(n²) - Quadratic Space

def create_matrix(n):
    matrix = []
    for i in range(n):
        row = []
        for j in range(n):
            row.append(i * j)
        matrix.append(row)
    return matrix  # Uses O(n²) space

Best, Average, and Worst Case

Linear Search Analysis

def linear_search_analysis(arr, target):
    comparisons = 0
    
    for element in arr:
        comparisons += 1
        if element == target:
            return comparisons  # Best case: 1, Worst case: n
    
    return comparisons  # Target not found: n
  • Best Case: O(1) - Target is first element
  • Average Case: O(n) - Target is middle element
  • Worst Case: O(n) - Target is last element or not found

Quick Sort Analysis

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    
    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]
    
    return quick_sort(left) + middle + quick_sort(right)
  • Best Case: O(n log n) - Pivot always divides array evenly
  • Average Case: O(n log n) - Random pivot selection
  • Worst Case: O(n²) - Pivot is always smallest or largest

Analyzing Algorithms

Steps for Big O Analysis

  1. Identify the basic operation that dominates runtime
  2. Express runtime as function of input size n
  3. Drop constant factors and lower-order terms
  4. Use Big O notation to describe growth rate

Example: Sum of Array Elements

def sum_array(arr):
    total = 0          # O(1)
    for element in arr:  # Runs n times
        total += element  # O(1)
    return total       # O(1)

Analysis:

  • Basic operation: addition (O(1))
  • Runs n times: n × O(1) = O(n)
  • Drop constants: O(n)

Example: Nested Loops

def print_pairs(arr):
    for i in range(len(arr)):      # n iterations
        for j in range(len(arr)):  # n iterations
            print(arr[i], arr[j])  # O(1)

Analysis:

  • Outer loop: n iterations
  • Inner loop: n iterations for each outer iteration
  • Total operations: n × n = n²
  • Complexity: O(n²)

Optimization Strategies

1. Algorithm Selection

Choose the right algorithm for the problem:

# O(n²) - Bubble sort (bad for large arrays)
def bubble_sort(arr):
    # ... implementation

# O(n log n) - Merge sort (better for large arrays)
def merge_sort(arr):
    # ... implementation

# O(n log n) - Quick sort (often fastest in practice)
def quick_sort(arr):
    # ... implementation

2. Data Structure Choice

Use appropriate data structures:

# O(n) - Linear search in list
def linear_search_list(arr, target):
    return target in arr

# O(1) - Hash table lookup
def hash_table_lookup(dict, target):
    return target in dict

3. Memoization

Cache results to avoid recomputation:

# O(2ⁿ) - Inefficient Fibonacci
def fibonacci_slow(n):
    if n <= 1:
        return n
    return fibonacci_slow(n - 1) + fibonacci_slow(n - 2)

# O(n) - Memoized Fibonacci
memo = {}
def fibonacci_fast(n):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    
    memo[n] = fibonacci_fast(n - 1) + fibonacci_fast(n - 2)
    return memo[n]

Common Pitfalls

1. Ignoring Hidden Complexity

# Looks like O(n) but actually O(n²)
def remove_duplicates(arr):
    result = []
    for element in arr:           # n iterations
        if element not in result:  # O(n) operation!
            result.append(element)
    return result

2. Confusing Best and Worst Case

Always analyze worst-case scenario unless specified otherwise.

3. Forgetting Space Complexity

Consider both time and space complexity in analysis.

Practical Guidelines

When to Optimize

  1. Profile first: Measure actual performance
  2. Identify bottlenecks: Focus on code that actually matters
  3. Consider constraints: Input size, memory limits, real-time requirements

Optimization Rules of Thumb

  1. O(n²) → O(n log n): Use better sorting algorithms
  2. O(n) → O(log n): Use binary search or hash tables
  3. O(2ⁿ) → O(n): Use dynamic programming or memoization
  4. Multiple O(n) → O(n): Combine linear passes

Real-World Examples

Database Indexing

  • Without index: O(n) - Full table scan
  • With index: O(log n) - B-tree lookup

Web Server Scaling

  • Single thread: O(1) per request
  • Multiple threads: O(n) - Linear scaling
  • Distributed system: O(log n) - Logarithmic scaling

Caching Strategies

  • Cache miss: O(n) - Fetch from database
  • Cache hit: O(1) - Fetch from memory

Testing Big O

Empirical Testing

import time
import random

def measure_runtime(func, sizes):
    for n in sizes:
        data = list(range(n))
        random.shuffle(data)
        
        start_time = time.time()
        func(data)
        end_time = time.time()
        
        print(f"n={n}, time={end_time - start_time:.6f}s")

# Test sorting algorithms
measure_runtime(bubble_sort, [100, 1000, 5000])
measure_runtime(merge_sort, [100, 1000, 5000])

Expected Results

  • O(n²): Time increases quadratically
  • O(n log n): Time increases faster than linear but slower than quadratic
  • O(n): Time increases linearly
Back to Blog
Share:

Related Posts