Python Algorithms & Data Structures track

Lesson 01 · Part 1: Foundations

Time Complexity

Big-O notation, runtime and memory limits, measuring performance

Part 1: Foundations5 min readFree lesson

Introduction

When solving algorithm problems, as important as getting the 'correct answer' is how fast you can get the answer. Most problems on Baekjoon Online Judge (BOJ) have time limits (1–2 seconds) and memory limits (128–256MB), so inefficient algorithms cannot pass. By understanding time complexity, you can predict whether 'this solution can pass?' before writing the code.


Learning Objectives

  1. You can explain the meaning of Big-O notation and major complexity classes.
  2. You can analyze the time complexity of given code snippets.
  3. You can determine the appropriate algorithm complexity by looking at the input size (N) of a Baekjoon problem.

Key Concepts

1. What is Big-O notation?

Big-O is a notation that describes how the running time (or memory usage) of an algorithm in the worst case increases as the input size N grows very large. Constant coefficients and lower-order terms are ignored.

Example: 3n² + 5n + 7 → O(n²)

2. Major Complexity Classes

Big-O Name Example Algorithm Approximate Operations for N=10⁶
O(1) Constant Array index access, hash lookup 1
O(log n) Logarithmic Binary search, Heap Insert/Delete ~20
O(n) Linear Array traversal, Linear search 1,000,000
O(n log n) Linear-Log Merge sort, Heap sort, sorted() ~20,000,000
O(n²) Quadratic Bubble sort, Insertion sort, Double loop 1,000,000,000,000
O(2ⁿ) Exponential Complete search of subsets Impossible

3. Time Limit and Number of Operations

Time limit 1 second ≈ about 100 million (10⁸) operations

Based on this standard, we judge the allowable complexity according to the input size:

N (Input Size) Allowable Complexity (1 second standard)
N ≤ 10 O(2ⁿ), O(n!)
N ≤ 20 O(2ⁿ)
N ≤ 500 O(n³)
N ≤ 3,000 O(n²)
N ≤ 100,000 O(n log n)
N ≤ 1,000,000 O(n)
N ≤ 10,000,000 O(n) (tight)

4. Memory Limit

In Python, a list of 1,000,000 integers uses about 8MB (4MB in C/C++ standard). Due to Python object overhead, it actually uses more memory, so caution is needed.

List of N integers: about 8N bytes (about 28 bytes/element including Python overhead for int)
2-dimensional N×N list: O(N²) space → if N=1000, about 8MB

5. Measuring Actual Time with timeit

import timeit

#Method 1: timeit.timeit() — Measure short code snippets
result = timeit.timeit('sum(range(1000))', number=10000)
print(f'Total time for 10000 executions: {result:.4f} seconds')

#Method 2: time module — Measure long code blocks
import time
start = time.perf_counter()
#... code to be measured ...
elapsed = time.perf_counter() - start
print(f'Execution time: {elapsed:.6f} seconds')

Examples

Example 1: Visualizing Each Complexity Class (src/01_big_o_demo.py)

Prints the actual number of operations for each complexity from O(1) to O(n²).

[Example of execution result]
O(1)     Number of operations:            1  (n=100000)
O(log n) Number of operations:           17  (n=100000)
O(n)     Number of operations:       100000  (n=100000)
O(n logn)Number of operations:      1660964  (n=100000, expected value)
O(n²)    Number of operations:       499500  (n=1000)
O(2ⁿ)    Number of operations:      1048576  (n=20)
Comparison of Complexity Growth (n=16)
╔═══════════════╦══════════════════════╗
║ Big-O         ║ Number of Operations (Approx.)        ║
╠═══════════════╬══════════════════════╣
║ O(1)          ║ 1                    ║
║ O(log n)      ║ 4                    ║
║ O(n)          ║ 16                   ║
║ O(n log n)    ║ 64                   ║
║ O(n²)         ║ 256                  ║
║ O(2ⁿ)         ║ 65536                ║
╚═══════════════╩══════════════════════╝

Try running it yourself:

python src/01_big_o_demo.py

Example 2: Actual Time Measurement (src/02_time_measure.py)

Compare the execution time by input size for bubble sort (O(n²)) and Python built-in sorted() (O(n log n)) using the timeit and time modules.

[Example of execution result]
Input Size    Bubble Sort (sec)   sorted() (sec)   Ratio
     100      0.001234      0.000023     53.7x
    1000      0.125600      0.000310    405.2x
   10000     12.340000      0.003500   3525.7x
python src/02_time_measure.py

Example 3: Space Complexity (src/03_space_complexity.py)

Measure O(1), O(n), and O(n²) space usage using sys.getsizeof.

python src/03_space_complexity.py

Example 4: Linear Search vs Binary Search (src/04_complexity_compare.py)

Measure the number of comparisons in linear search (O(n)) and binary search (O(log n)) on the same data using a counter.

python src/04_complexity_compare.py

Common Mistakes

  1. Applying an O(n²) solution to n=100,000
  • O(n²) at n=100,000 is about 10¹⁰ operations → time limit exceeded is inevitable
  • Whenever you see a nested loop, always check the size of n.
  1. Python is 5~10 times slower than C++
  • Baekjoon Python time limits sometimes allow 3~5 times more than C++, but not always.
  • Even for O(n log n) solutions, you may need to use PyPy to pass.
  1. Ignoring constants leads to TLE
  • Even O(n) can be slow if the constants are large.
  • Example: Concatenating strings (+=) inside a loop looks like O(n) but is actually O(n²).
  1. Using the in Operator on a List
  • x in list → O(n), x in set → O(1) on average
  • When the membership check is repeated, be sure to convert it to set.
  1. Submit without complexity analysis
  • Before writing the code, 'the complexity of this solution is O(?) and how many operations when n=?' Always cultivate the habit of calculating.

Summary

  • Big-O represents the growth rate of execution time in the worst case.
  • The time limit is 1 second ≈ choose complexity based on 10⁸ operations.
  • Python is slower than C++, so a more efficient algorithm may be needed.
  • The in operation varies between O(n) vs O(1) depending on the data structure.

Try it yourself

  1. Run src/01_big_o_demo.py and record the number of operations for each complexity when n=100.
  2. Try increasing the input size to 100,000 in src/02_time_measure.py and see how much the bubble sort Check if it takes time (Warning: It may take a very long time).
  3. Solve Baekjoon Problem 1546 (Average), Analyze the complexity of your own solution.
  4. Complete the assignment below.

Reference Materials

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/01_big_o_demo.pyPython
# =============================================================================
#File name: 01_big_o_demo.py
#Description: Visualize the main Big-O complexity classes with the actual number of operations
#Time Complexity: See comments for each function
#Space Complexity: O(1) — No additional space used
# =============================================================================

import math


# ---------------------------------------------------------------------------
#O(1) — Constant time: Always the same number of operations regardless of input size
# ---------------------------------------------------------------------------
def o_constant(arr: list) -> tuple:
    """Returns the first element of the array. Time complexity: O(1)"""
    count = 0
    result = arr[0]  #Only one access
    count += 1
    return result, count


# ---------------------------------------------------------------------------
#O(log n) — Logarithmic time: Halve the search range at each step
# ---------------------------------------------------------------------------
def o_log_n(n: int) -> int:
    """Returns the number of steps to reach 1 using the binary search method. Time complexity: O(log n)"""
    count = 0
    current = n
    while current > 1:
        current //= 2  #Halve it
        count += 1
    return count


# ---------------------------------------------------------------------------
#O(n) — Linear time: Visit each element of the array once
# ---------------------------------------------------------------------------
def o_linear(arr: list) -> tuple:
    """Calculates the sum of an array. Time complexity: O(n)"""
    count = 0
    total = 0
    for x in arr:  #Repeat n times
        total += x
        count += 1
    return total, count


# ---------------------------------------------------------------------------
#O(n log n) — Linearithmic time: Perform log steps for each element
# ---------------------------------------------------------------------------
def o_n_log_n_estimate(n: int) -> int:
    """Returns the expected number of operations for n log n. Time complexity: O(n log n)"""
    if n <= 1:
        return 1
    return int(n * math.log2(n))


# ---------------------------------------------------------------------------
#O(n²) — Quadratic time: Nested loops (check all pairs)
# ---------------------------------------------------------------------------
def o_quadratic(arr: list) -> tuple:
    """Count the cases where the sum of all element pairs is 0. Time complexity: O(n²)"""
    count = 0
    pairs = 0
    n = len(arr)
    for i in range(n):              #Outer loop: n times
        for j in range(i + 1, n):  #Inner loop: n-i-1 times
            count += 1
            if arr[i] + arr[j] == 0:
                pairs += 1
    return pairs, count


# ---------------------------------------------------------------------------
#O(2ⁿ) — Exponential time: Enumerate all subsets (only feasible for small n)
# ---------------------------------------------------------------------------
def o_exponential(n: int) -> int:
    """Returns the number of all subsets of n elements. Time complexity: O(2ⁿ)"""
    count = 0
    for mask in range(1 << n):  #2^n cases
        count += 1
    return count


# ---------------------------------------------------------------------------
#Display table comparing growth of complexities
# ---------------------------------------------------------------------------
def print_complexity_table(n: int) -> None:
    """When n is given, the number of operations for each complexity is printed in a table."""
    log_n = int(math.log2(n)) if n > 1 else 1
    n_log_n = int(n * math.log2(n)) if n > 1 else n
    n_sq = n * n
    two_n = 2 ** n if n <= 30 else None

    print(f"Complexity growth comparison (n={n})")
    print("╔═══════════════╦══════════════════════╗")
    print("║ Big-O         ║ Number of Operations (Approx.)        ║")
    print("╠═══════════════╬══════════════════════╣")
    print(f"║ O(1)          ║ {1:<20} ║")
    print(f"║ O(log n)      ║ {log_n:<20} ║")
    print(f"║ O(n)          ║ {n:<20} ║")
    print(f"║ O(n log n)    ║ {n_log_n:<20} ║")
    print(f"║ O(n²)         ║ {n_sq:<20} ║")
    if two_n is None:
        print(f"║ O(2ⁿ)         ║ {'Too large (uncomputable)':<20} ║")
    else:
        print(f"║ O(2ⁿ)         ║ {two_n:<20} ║")
    print("╚═══════════════╩══════════════════════╝")


def main():
    print("=" * 55)
    print("Big-O complexity demo")
    print("=" * 55)

    n = 100_000
    arr_large = list(range(n))

    #O(1) demo
    _, cnt = o_constant(arr_large)
    print(f"O(1)     Number of operations: {cnt:>12}  (n={n})")

    #O(log n) demo
    cnt = o_log_n(n)
    print(f"O(log n) Number of operations: {cnt:>12}  (n={n})")

    #O(n) demo
    _, cnt = o_linear(arr_large)
    print(f"O(n)     Number of operations: {cnt:>12}  (n={n})")

    #O(n log n) demo (expected value)
    cnt = o_n_log_n_estimate(n)
    print(f"O(n logn)Number of operations: {cnt:>12}  (n={n}, expected value)")

    #O(n²) — Measure with a small array
    n_small = 1000
    arr_mid = list(range(-n_small // 2, n_small // 2))
    _, cnt = o_quadratic(arr_mid)
    print(f"O(n²)    Number of operations: {cnt:>12}  (n={n_small})")

    #O(2ⁿ) — Only possible for very small n
    n_exp = 20
    cnt = o_exponential(n_exp)
    print(f"O(2ⁿ)    Number of operations: {cnt:>12}  (n={n_exp})")

    #Table comparing growth of complexities
    print_complexity_table(16)
    print_complexity_table(1000)
    print_complexity_table(1_000_000)

    print("[Note] Time limit 1 second ≈ 10^8 operations")
    print("[Note] If n=100,000, O(n log n) ≈ 1.7×10^6 → safe")
    print("[Note] If n=100,000, O(n²)      ≈ 10^10  → time limit exceeded")


if __name__ == "__main__":
    main()
src/02_time_measure.pyPython
# =============================================================================
#Filename: 02_time_measure.py
#Description: Compare bubble sort (O(n²)) and built-in sorted() (O(n log n)) using the timeit / time modules
#Time complexity: Bubble sort O(n²), sorted() O(n log n)
#Space complexity: O(n) — array to be sorted
# =============================================================================

import time
import timeit
import random
import copy


# ---------------------------------------------------------------------------
#Bubble sort — O(n²)
# ---------------------------------------------------------------------------
def bubble_sort(arr: list) -> list:
    """Implement bubble sort. Time complexity: O(n²), space complexity: O(1)"""
    a = arr[:]          #Copy to preserve original
    n = len(a)
    for i in range(n):              #Outer loop: n times
        swapped = False
        for j in range(n - i - 1):  #Inner loop: n-i-1 times
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]  #Swap
                swapped = True
        if not swapped:             #Already sorted → Early exit optimization
            break
    return a


# ---------------------------------------------------------------------------
#Insertion sort — O(n²) average, O(n) best
# ---------------------------------------------------------------------------
def insertion_sort(arr: list) -> list:
    """Insertion sort implementation. Time complexity: O(n²) average, space complexity: O(1)"""
    a = arr[:]
    for i in range(1, len(a)):       #From the first to the last element
        key = a[i]
        j = i - 1
        while j >= 0 and a[j] > key:  #Find the appropriate position
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = key
    return a


# ---------------------------------------------------------------------------
#Time measurement utility
# ---------------------------------------------------------------------------
def measure_time(func, arr: list) -> float:
    """Returns the function execution time in seconds."""
    data = copy.deepcopy(arr)
    start = time.perf_counter()
    func(data)
    return time.perf_counter() - start


def benchmark(sizes: list) -> None:
    """Compares the execution times of sorting algorithms for various input sizes."""
    print("\n" + "=" * 70)
    print("Sorting algorithm performance comparison")
    print("=" * 70)
    header = (
        f"{'Input size':>10}"
        f"{'Bubble sort (sec)':>14}"
        f"{'Insertion sort (sec)':>14}"
        f"{'sorted() (sec)':>14}"
    )
    print(header)
    print("-" * 70)

    for size in sizes:
        random.seed(42)
        arr = [random.randint(1, 10 ** 6) for _ in range(size)]

        t_bubble = measure_time(bubble_sort, arr)
        t_insert = measure_time(insertion_sort, arr)

        #sorted() is Timsort — O(n log n)
        start = time.perf_counter()
        sorted(arr)
        t_sorted = time.perf_counter() - start

        ratio_b = t_bubble / t_sorted if t_sorted > 0 else float('inf')
        ratio_i = t_insert / t_sorted if t_sorted > 0 else float('inf')

        print(
            f"{size:>10,}  "
            f"{t_bubble:>13.6f}s "
            f"{t_insert:>13.6f}s "
            f"{t_sorted:>13.6f}s"
        )
        print(
            f"{'':>10}  "
            f"({ratio_b:>6.1f}x slower)"
            f"({ratio_i:>6.1f}x slower)"
        )

    print("=" * 70)
    print("[Conclusion] O(n²) algorithms get significantly slower as n increases.")
    print("For n=10,000 or more, O(n log n) should be used.")


def main():
    print("timeit module example — measure performance of short code")
    print("-" * 40)

    #Compare list comprehension vs loop with timeit
    t_comp = timeit.timeit(
        '[x**2 for x in range(1000)]',
        number=1000
    )
    t_loop = timeit.timeit(
        stmt=(
            'result = []\n'
            'for x in range(1000):\n'
            '    result.append(x**2)'
        ),
        number=1000
    )
    print(f"List comprehension (1000 times): {t_comp:.4f} sec")
    print(f"for loop + append (1000 times): {t_loop:.4f} seconds")
    if t_comp > 0:
        print(f"Comprehension is {t_loop/t_comp:.2f} times faster")

    #Sorting algorithm benchmark
    benchmark(sizes=[100, 1000, 5000])


if __name__ == "__main__":
    main()
src/03_space_complexity.pyPython
# =============================================================================
#Filename: 03_space_complexity.py
#Description: Measure O(1), O(n), O(n²) space complexity using sys.getsizeof
#Time complexity: O(n²) — when creating a 2D array
#Space complexity: see comments for each function
# =============================================================================

import sys


# ---------------------------------------------------------------------------
#O(1) space — uses only a fixed number of variables
# ---------------------------------------------------------------------------
def sum_o1_space(arr: list) -> int:
    """Sum calculation of an array. Space complexity: O(1) — using only 2 variables"""
    total = 0       #One variable
    for x in arr:   #One loop variable (stack usage ignored)
        total += x
    return total


# ---------------------------------------------------------------------------
#O(n) space — additional space proportional to input size
# ---------------------------------------------------------------------------
def prefix_sum_on_space(arr: list) -> list:
    """Return the cumulative sum array. Space complexity: O(n) — result array of n elements"""
    n = len(arr)
    prefix = [0] * (n + 1)  #n+1 additional spaces
    for i in range(n):
        prefix[i + 1] = prefix[i] + arr[i]
    return prefix


# ---------------------------------------------------------------------------
#O(n²) space — 2D array (n × n)
# ---------------------------------------------------------------------------
def create_matrix_on2_space(n: int) -> list:
    """Create an n×n matrix. Space complexity: O(n²)"""
    matrix = [[0] * n for _ in range(n)]  #n*n spaces
    for i in range(n):
        matrix[i][i] = 1  #Identity matrix form
    return matrix


# ---------------------------------------------------------------------------
#Recursive call stack space — O(n) (equal to the depth of calls)
# ---------------------------------------------------------------------------
def recursive_factorial(n: int) -> int:
    """Recursive factorial. Space complexity: O(n) — call stack depth n"""
    if n <= 1:
        return 1
    return n * recursive_factorial(n - 1)  #n stack frames stacked


def iterative_factorial(n: int) -> int:
    """Iterative factorial. Space complexity: O(1) — no additional space"""
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result


# ---------------------------------------------------------------------------
#Memory usage measurement helper
# ---------------------------------------------------------------------------
def list_total_size(obj: list) -> int:
    """Calculates the total memory usage of the list (header + all elements)."""
    if not isinstance(obj, list):
        return sys.getsizeof(obj)
    total = sys.getsizeof(obj)
    for item in obj:
        total += sys.getsizeof(item)
    return total


def main():
    print("=" * 55)
    print("Demonstration of space complexity")
    print("=" * 55)

    sizes = [10, 100, 1000]

    # -----------------------------------------------------------------------
    #Measure O(1) space
    # -----------------------------------------------------------------------
    print("[O(1) space] Variable usage (independent of input size)")
    for n in sizes:
        print(f"n={n:>5}: uses only 2 additional variables → constant space")

    # -----------------------------------------------------------------------
    #Measure O(n) space
    # -----------------------------------------------------------------------
    print("[O(n) space] Size of cumulative sum array")
    for n in sizes:
        arr = list(range(n))
        prefix = prefix_sum_on_space(arr)
        total_bytes = list_total_size(prefix)
        print(
            f"n={n:>5}: prefix array size ="
            f"{len(prefix):>5},"
            f"Total {total_bytes:>7} bytes"
            f"(≈ {total_bytes/1024:.1f} KB)"
        )

    # -----------------------------------------------------------------------
    #Measure O(n²) space
    # -----------------------------------------------------------------------
    print("[O(n²) space] n×n matrix size")
    for n in [10, 100, 300]:
        matrix = create_matrix_on2_space(n)
        total_bytes = sys.getsizeof(matrix)
        for row in matrix:
            total_bytes += sys.getsizeof(row)
            total_bytes += sum(sys.getsizeof(x) for x in row)
        print(
            f"n={n:>4}: {n}×{n} matrix,"
            f"Number of elements = {n*n:>6},"
            f"Total {total_bytes:>9} bytes"
            f"(≈ {total_bytes/1024:.1f} KB)"
        )

    # -----------------------------------------------------------------------
    #Recursion vs iteration comparison
    # -----------------------------------------------------------------------
    print("[Recursion vs Iteration] Factorial space comparison")
    n = 10
    rec = recursive_factorial(n)
    itr = iterative_factorial(n)
    print(f"Recursive factorial({n}!) = {rec}  → call stack {n} levels (O(n) space)")
    print(f"Iterative factorial({n}!) = {itr}  → only 1 variable used (O(1) space)")

    # -----------------------------------------------------------------------
    #Reference for Python integer size
    # -----------------------------------------------------------------------
    print("[Reference] Python object size")
    print(f"int (small value 28):   {sys.getsizeof(28)} bytes")
    print(f"int (large value 10^18):  {sys.getsizeof(10**18)} bytes")
    print(f"  float:              {sys.getsizeof(1.0)} bytes")
    print(f"empty list []:         {sys.getsizeof([])} bytes")
    print(f"list with 1000 elements:        {sys.getsizeof([0]*1000)} bytes (header only)")
    print("[Conclusion] Python int is much larger than C's int (4 bytes).")
    print("Be careful about memory limits when processing large data.")


if __name__ == "__main__":
    main()
src/04_complexity_compare.pyPython
# =============================================================================
#Filename: 04_complexity_compare.py
#Description: Linear search O(n) vs Binary search O(log n) comparison (measure operation count with counter)
#Time complexity: Linear search O(n), Binary search O(log n)
#Space complexity: O(1) — no additional space
# =============================================================================

import math
import time
import random


# ---------------------------------------------------------------------------
#Linear search — O(n)
#Search the array sequentially from the front
# ---------------------------------------------------------------------------
def linear_search(arr: list, target: int) -> tuple:
    """Returns the index of the target and the number of comparisons using linear search.

    Time complexity: O(n) — n comparisons in the worst case
    Space complexity: O(1)"""
    count = 0
    for i, val in enumerate(arr):  #Sequentially from the front
        count += 1
        if val == target:
            return i, count
    return -1, count  #If not found


# ---------------------------------------------------------------------------
#Binary search — O(log n)
#In a sorted array, compare with the middle value and narrow the range by half each time
# ---------------------------------------------------------------------------
def binary_search(arr: list, target: int) -> tuple:
    """Returns the index of the target and the number of comparisons using binary search.

    Preconditions: arr must be sorted in ascending order.
    Time complexity: O(log n) — the search range is halved at each step
    Space complexity: O(1)"""
    count = 0
    left, right = 0, len(arr) - 1

    while left <= right:
        mid = (left + right) // 2  #Middle index
        count += 1

        if arr[mid] == target:
            return mid, count       #Found
        elif arr[mid] < target:
            left = mid + 1          #Move to the right half
        else:
            right = mid - 1         #Move to the left half

    return -1, count  #If not found


# ---------------------------------------------------------------------------
#Visualization: Binary search step-by-step output
# ---------------------------------------------------------------------------
def binary_search_verbose(arr: list, target: int) -> int:
    """It prints the binary search process step by step."""
    left, right = 0, len(arr) - 1
    step = 0

    print(f"Search target: {target}")
    print(f"Array size: {len(arr)}")

    while left <= right:
        mid = (left + right) // 2
        step += 1
        print(
            f"Step {step}: left={left}, right={right},"
            f"mid={mid}, arr[mid]={arr[mid]}"
        )

        if arr[mid] == target:
            print(f"→ Found! Index {mid}, total {step} steps")
            return mid
        elif arr[mid] < target:
            left = mid + 1
            print(f"arr[mid] < target → move to the right")
        else:
            right = mid - 1
            print(f"arr[mid] > target → move to the left")

    print(f"→ Not found, total {step} steps")
    return -1


# ---------------------------------------------------------------------------
#Comparison benchmark
# ---------------------------------------------------------------------------
def compare_benchmark(sizes: list) -> None:
    """Compares two search algorithms on various input sizes."""
    print("\n" + "=" * 70)
    print("Linear search O(n) vs Binary search O(log n) comparison")
    print("=" * 70)
    print(
        f"{'n':>10}  {'Linear comparison count':>14}"
        f"{'Binary comparison count':>16} {'Ratio':>8}"
    )
    print("-" * 70)

    for n in sizes:
        arr = list(range(n))       #Sorted array from 0 to n-1
        target = n - 1              #Worst case: last element

        _, linear_count = linear_search(arr, target)
        _, binary_count = binary_search(arr, target)
        expected_log = int(math.log2(n)) + 1 if n > 1 else 1

        ratio = linear_count / binary_count if binary_count > 0 else 0
        print(
            f"{n:>10,}  "
            f"{linear_count:>14,} "
            f"{binary_count:>10,} (Expected log₂n≈{expected_log})"
            f"{ratio:>8.1f}x"
        )

    print("=" * 70)


def main():
    print("=" * 55)
    print("Search algorithm complexity comparison demo")
    print("=" * 55)

    # -----------------------------------------------------------------------
    #Binary search visualization (small array)
    # -----------------------------------------------------------------------
    print("[Binary search step-by-step visualization]")
    small_arr = list(range(0, 32, 2))  # [0, 2, 4, ..., 62]
    print(f"Array: {small_arr}")
    binary_search_verbose(small_arr, 42)

    # -----------------------------------------------------------------------
    #Verify search result accuracy
    # -----------------------------------------------------------------------
    print("[Result verification]")
    random.seed(42)
    test_arr = sorted(random.sample(range(1, 1001), 50))
    target = test_arr[25]  #Select middle value of the array

    lin_idx, lin_cnt = linear_search(test_arr, target)
    bin_idx, bin_cnt = binary_search(test_arr, target)

    print(f"Array size 50, target={target}")
    print(f"Linear search: Index={lin_idx}, Comparison count={lin_cnt}")
    print(f"Binary search: Index={bin_idx}, Comparison count={bin_cnt}")
    print(f"Result match: {lin_idx == bin_idx}")

    # -----------------------------------------------------------------------
    #Benchmark table
    # -----------------------------------------------------------------------
    compare_benchmark([10, 100, 1_000, 10_000, 100_000, 1_000_000])

    # -----------------------------------------------------------------------
    #Execution time comparison
    # -----------------------------------------------------------------------
    print("[Execution time comparison] n=1,000,000, Worst-case 1,000 searches")
    big_arr = list(range(1_000_000))
    target = 999_999
    repeat = 1000

    start = time.perf_counter()
    for _ in range(repeat):
        linear_search(big_arr, target)
    t_linear = time.perf_counter() - start

    start = time.perf_counter()
    for _ in range(repeat):
        binary_search(big_arr, target)
    t_binary = time.perf_counter() - start

    print(f"Linear search {repeat} times: {t_linear:.4f} seconds")
    print(f"Binary search {repeat} times: {t_binary:.6f} seconds")
    if t_binary > 0:
        print(f"Binary search is {t_linear/t_binary:.0f} times faster")


if __name__ == "__main__":
    main()

Assignment

Solve the three problems below and indicate the time complexity and space complexity for each solution in comments.


Problem 1: Baekjoon 1546 — Average

Problem Summary

Sejun wants to manipulate the scores as follows.

  1. Find the maximum M among the subject scores.
  2. Convert all scores to Score/M-100.
  3. Output the average of the new scores.

Input: The first line contains N (1 ≤ N ≤ 1000), the second line contains N scores (0 ≤ score ≤ 100)

Complexity Analysis Hint

Step Operation Complexity
Find maximum Traverse array once O(n)
Compute sum Traverse array once O(n)
Total Traverse array twice O(n)
  • Since N ≤ 1,000, any solution passes, but check if it can be solved in O(n).

Submission File

boj_1546.py


Problem 2: Baekjoon 2750 — Sort Numbers

Problem Summary

Sort and print N numbers.

Input: N (1 ≤ N ≤ 1,000), followed by N integers (one per line)

Assignment Requirements

  1. First, implement using Bubble Sort (O(n²)).
  2. For the same input, also implement using sorted() (O(n log n)).
  3. Write both codes in boj_2750.py and analyze in comments which one is faster.

Complexity Comparison

N = 1,000
Bubble Sort: N² = 1,000,000 operations → can be slow
sorted(): N log N ≈ 10,000 operations → fast

Since N ≤ 1,000, Bubble Sort also passes,
but if N = 100,000, Bubble Sort will TLE!

Submission Files

boj_2750.py


Problem 3: Baekjoon 10989 — Sort Numbers 3

Problem Summary

Sort and print N numbers.

Input: N (1 ≤ N ≤ 10,000,000), followed by N integers (1 ≤ value ≤ 10,000)

Why is it difficult?

Approach Complexity When N=10,000,000 Result
sorted() O(n log n) ~230 million operations MLE/TLE (Python)
Bubble Sort O(n²) 10¹⁴ operations TLE
Counting Sort O(n + k) ~10 million operations Pass
  • Memory Issue: Storing all N=10,000,000 numbers in a list will exceed memory!
  • Solution: Since the range of values (1~10,000) is small, use only a counter array (size 10,001)

Principle of Counting Sort

Input: [3, 1, 4, 1, 5, 9, 2, 6]

Counter array (index = value):
count[1] = 2  (1 appears 2 times)
count[2] = 1
count[3] = 1
...

Output: print 1 two times, 2 one time, 3 one time, ... in order
→ 1 1 2 3 4 5 6 9

Precautions

  • To avoid TLE, input should be received using sys.stdin.
  • Output should also be printed at once using sys.stdout.write or '\\n'.join().

Submission Files

boj_10989.py


Submission Checklist

  • Write time complexity and space complexity comments at the top of each file
  • Ensure code properly receives Correct answer on Baekjoon
  • Follow PEP 8 style (4-space indent, line length within 79 characters)
  • Include comments with your own analysis of the solution method and complexity

References

  • Counting Sort: Wikipedia
  • Keywords for reference solution of Baekjoon problem 10989: sys.stdin, counting sort
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

boj_10989.pyPython
# =============================================================================
#Baekjoon 10989: Sorting Numbers 3
#Link: https://www.acmicpc.net/problem/10989
#Time complexity: O(n + k) — reading n numbers + printing in the range k=10,000 (Counting Sort)
#Space complexity: O(k) — counter array size 10,001 (input array not stored)
#
#Key point: N=10,000,000, memory limit 8MB
#- Storing all numbers in a list causes MLE → use only counter array
#- Reading 10 million times using input() causes TLE → use sys.stdin
#- Printing 10 million times using print() causes TLE → use sys.stdout.write
# =============================================================================

import sys


def solve():
    # -----------------------------------------------------------------------
    #Fast input: read all at once with sys.stdin.buffer.read() (byte handling is faster)
    #Calling input() 10 million times causes TLE
    # -----------------------------------------------------------------------
    data = sys.stdin.buffer.read().split()
    n = int(data[0])

    # -----------------------------------------------------------------------
    #Counting Sort
    #Range of values: 1 ≤ x ≤ 10,000
    #count[v] = number of times v appears
    # -----------------------------------------------------------------------
    k = 10_001
    count = [0] * k  #O(k) space — only store 10,001 integers

    #Count occurrences of each number — O(n)
    for i in range(1, n + 1):
        val = int(data[i])
        count[val] += 1

    # -----------------------------------------------------------------------
    #Fast output: collect in a list and print all at once
    #Calling print() 10 million times causes TLE
    # -----------------------------------------------------------------------
    output = []
    for v in range(1, k):       #From 1 to 10,000
        if count[v] > 0:
            #Print v count[v] times (use string repetition)
            output.append((str(v) + '\n') * count[v])

    sys.stdout.write(''.join(output))


if __name__ == "__main__":
    solve()
boj_1546.pyPython
# =============================================================================
#Baekjoon 1546: Average
#Link: https://www.acmicpc.net/problem/1546
#Time Complexity: O(n) — traverse the array twice (for max value and sum calculation)
#Space Complexity: O(n) — storing the score array
# =============================================================================
#Solution Strategy:
#1. Read N scores.
#2. Find the maximum value M. ← O(n)
#3. Convert each score to (score / M * 100) and then calculate the average. ← O(n)
#Since N ≤ 1,000, any solution passes, but O(n) is ideal.
# =============================================================================

import sys


def solve():
    input_data = sys.stdin.read().split()
    n = int(input_data[0])                    #Number of subjects
    scores = list(map(int, input_data[1:n + 1]))  #List of scores

    #Find maximum — O(n)
    max_score = max(scores)

    #Calculate sum of converted scores — O(n)
    #Conversion formula: new_score = score / max_score * 100
    #Average = sum(new_scores) / n
    #       = sum(score / max_score * 100 for score in scores) / n
    #       = (sum(scores) * 100 / max_score) / n
    total = sum(scores)                        #Original score sum
    average = total / max_score * 100 / n     #Average after conversion

    print(f"{average:.12f}")  #Print 12 decimal places (within acceptable error range)


if __name__ == "__main__":
    solve()
boj_2750.pyPython
# =============================================================================
#Baekjoon 2750: Sorting Numbers
#Link: https://www.acmicpc.net/problem/2750
#Method 1 — Bubble Sort:
#Time complexity: O(n²) — nested loop
#Space complexity: O(1) — in-place sorting
#Method 2 — sorted():
#Time complexity: O(n log n) — Timsort algorithm
#Space complexity: O(n) — array of sorted results
#
#Analysis: Since N ≤ 1,000, both methods pass.
#However, if N = 100,000, bubble sort will TLE, but sorted() passes.
# =============================================================================

import sys


# ---------------------------------------------------------------------------
#Method 1: Bubble Sort — O(n²)
# ---------------------------------------------------------------------------
def bubble_sort(arr: list) -> list:
    """Implement bubble sort. Time complexity: O(n²), space complexity: O(1)"""
    a = arr[:]          #Preserve original
    n = len(a)
    for i in range(n):
        for j in range(n - i - 1):
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]  #Swap adjacent elements
    return a


# ---------------------------------------------------------------------------
#Method 2: built-in sorted() function — O(n log n) (Timsort)
# ---------------------------------------------------------------------------
def builtin_sort(arr: list) -> list:
    """Python built-in sort. Time complexity: O(n log n) (Timsort)"""
    return sorted(arr)


def solve():
    data = sys.stdin.read().split()
    n = int(data[0])
    nums = list(map(int, data[1:n + 1]))

    #Sort using both methods (same result)
    result_bubble = bubble_sort(nums)
    result_builtin = builtin_sort(nums)

    #Verify results
    assert result_bubble == result_builtin, "The two sorting results are different!"

    #Output for Baekjoon submission (using sorted() result)
    sys.stdout.write('\n'.join(map(str, result_builtin)) + '\n')


if __name__ == "__main__":
    solve()

Learn this lesson with a mentor

Work through the Python Algorithms & Data Structures track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now