IT /알고리즘

시간 복잡도와 공간 복잡도: 알고리즘 효율성 분석

· 6분 읽기
#알고리즘 #복잡도 #Big-O #시간복잡도 #공간복잡도

시간 복잡도와 공간 복잡도: 알고리즘 효율성 분석

알고리즘의 성능을 평가하는 것은 매우 중요합니다. 같은 문제를 푸는 두 알고리즘이 있다면, 어느 것이 더 빠르고 효율적인지 알아야 합니다. 이번 글에서는 알고리즘의 효율성을 측정하는 시간 복잡도공간 복잡도에 대해 자세히 알아보겠습니다.

1. 왜 복잡도 분석이 필요한가?

현실적인 문제 상황

# 방법 1: 단순한 방법
def find_number_v1(arr, target):
    for num in arr:      # O(n)
        if num == target:
            return True
    return False

# 방법 2: 정렬 후 검색
def find_number_v2(arr, target):
    arr.sort()           # O(n log n)
    # 이진 검색 수행     # O(log n)
    # 총 시간: O(n log n)

어떤 방법을 선택해야 할까요? 데이터 크기에 따라 다릅니다!

복잡도 분석의 목적

  1. 알고리즘 비교: 같은 문제를 푸는 여러 알고리즘 중 최선 선택
  2. 성능 예측: 입력 크기가 커졌을 때 실행 시간 예측
  3. 최적화: 병목 지점 발견 및 개선
  4. 자원 계획: 메모리 사용량 예측

2. 시간 복잡도 (Time Complexity)

시간 복잡도는 알고리즘이 실행되는 데 걸리는 시간을 입력 크기의 함수로 표현한 것입니다.

실행 시간 측정 방법

1. 실제 실행 시간 측정

import time

def measure_time(func, *args):
    start = time.time()
    result = func(*args)
    end = time.time()
    return result, end - start

# 예시
arr = list(range(10000))
result, exec_time = measure_time(find_number_v1, arr, 5000)
print(f"실행 시간: {exec_time:.6f}초")

문제점:

  • 컴퓨터 성능에 따라 결과가 다름
  • 운영체제, 다른 프로세스 영향
  • 작은 입력에서는 차이가 미미함

2. 연산 횟수 세기

def count_operations(arr):
    count = 0
    for i in range(len(arr)):  # n번 반복
        count += 1             # 1번 연산
    return count

# 입력 크기가 n일 때 연산 횟수 = n

장점:

  • 입력 크기에 따른 패턴 파악 가능
  • 하드웨어 독립적

Big O 표기법

Big O 표기법은 **알고리즘의 상한(upper bound)**을 표현합니다.

주요 복잡도 클래스

Big O이름예시설명
O(1)상수 시간배열 인덱스 접근입력 크기와 무관
O(log n)로그 시간이진 검색입력 크기가 커져도 느리게 증가
O(n)선형 시간선형 검색입력 크기에 비례
O(n log n)선형로그 시간퀵 정렬, 병합 정렬대부분의 효율적인 정렬
O(n²)제곱 시간버블 정렬, 선택 정렬중첩 반복문
O(2ⁿ)지수 시간피보나치(재귀)매우 비효율적

Big O 계산 규칙

  1. 상수항 무시: O(2n) → O(n)
  2. 계수 무시: O(3n²) → O(n²)
  3. 최고차항만: O(n² + n) → O(n²)
  4. 로그의 밑은 무시: O(log₂ n) = O(log₁₀ n) = O(log n)

시간 복잡도 분석 예시

예시 1: 상수 시간 O(1)

def get_first_element(arr):
    return arr[0]  # 항상 1번의 연산

예시 2: 선형 시간 O(n)

def find_max(arr):
    max_val = arr[0]
    for num in arr:        # n번 반복
        if num > max_val:  # 1번 비교
            max_val = num
    return max_val         # 총 연산: O(n)

예시 3: 제곱 시간 O(n²)

def bubble_sort(arr):
    for i in range(len(arr)):        # n번
        for j in range(len(arr)-1):  # n번
            if arr[j] > arr[j+1]:    # 1번 비교
                swap(arr, j, j+1)    # 총 연산: O(n²)

예시 4: 로그 시간 O(log n)

def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:             # 최대 log₂n번 반복
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

최선/최악/평균 경우 분석

# 선형 검색의 경우 분석
def linear_search(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# 최선 경우 (Best Case): O(1)
# - 찾는 값이 배열의 첫 번째에 있을 때

# 최악 경우 (Worst Case): O(n)
# - 찾는 값이 배열의 마지막에 있거나 없을 때

# 평균 경우 (Average Case): O(n/2) = O(n)
# - 모든 위치에 있을 확률이 같다고 가정

3. 공간 복잡도 (Space Complexity)

공간 복잡도는 알고리즘이 사용하는 메모리 공간을 입력 크기의 함수로 표현한 것입니다.

공간 복잡도 분석 예시

예시 1: O(1) - 제자리 정렬

def swap_sort(arr):
    for i in range(len(arr)):
        for j in range(i+1, len(arr)):
            if arr[i] > arr[j]:
                arr[i], arr[j] = arr[j], arr[i]  # 임시 변수만 사용
    return arr

# 추가 공간: O(1) - 임시 변수만 사용

예시 2: O(n) - 추가 배열 사용

def copy_and_sort(arr):
    copied = arr.copy()  # O(n) 공간 추가
    return sorted(copied)

# 추가 공간: O(n) - 배열 복사본 생성

예시 3: O(n²) - 2차원 배열

def create_multiplication_table(n):
    table = [[0] * n for _ in range(n)]  # n x n 크기의 2차원 배열
    for i in range(n):
        for j in range(n):
            table[i][j] = (i+1) * (j+1)
    return table

# 공간 복잡도: O(n²)

4. 복잡도 분석 실전 팁

1. 재귀 함수의 복잡도

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

# 시간 복잡도: O(2ⁿ) - 지수 시간 (매우 비효율적)
# 공간 복잡도: O(n) - 콜 스택 깊이

2. 최적화된 재귀

def fibonacci_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
    return memo[n]

# 시간 복잡도: O(n) - 메모이제이션으로 최적화
# 공간 복잡도: O(n) - 메모 배열 + 콜 스택

3. 복잡도 비교 실습

import time
import matplotlib.pyplot as plt

def measure_complexity():
    sizes = [100, 500, 1000, 2000, 5000]

    for n in sizes:
        arr = list(range(n))

        # O(n) 알고리즘
        start = time.time()
        linear_sum = sum(arr)
        linear_time = time.time() - start

        # O(n²) 알고리즘 (제곱 시간 예시)
        start = time.time()
        quadratic_sum = 0
        for i in range(n):
            for j in range(n):
                quadratic_sum += 1
        quadratic_time = time.time() - start

        print(f"n={n}: O(n)={linear_time:.4f}s, O(n²)={quadratic_time:.4f}s")

measure_complexity()

5. 알고리즘 선택 가이드라인

입력 크기에 따른 선택

입력 크기허용되는 복잡도예시 알고리즘
n ≤ 10O(n!)완전 탐색
n ≤ 20O(2ⁿ)비트 마스크, DP
n ≤ 500O(n³)플로이드-워셜
n ≤ 5000O(n²)동적 계획법
n ≤ 10⁶O(n log n)정렬, 힙
n ≤ 10⁹O(n)선형 탐색
n > 10⁹O(log n), O(1)해시, 이진 검색

메모리 제약 고려

  • 제자리 알고리즘: 추가 공간 O(1)
  • 메모리 제한: 보통 256MB ~ 1GB
  • 공간-시간 트레이드오프: 메모리를 사용해서 시간을 절약할 수 있음

6. 연습 문제

  1. 다음 코드의 시간 복잡도를 분석하세요:
def mystery_function(arr):
    total = 0
    for i in range(len(arr)):
        for j in range(i, len(arr)):
            total += arr[i] + arr[j]
    return total
  1. 다음 중 시간 복잡도가 가장 낮은 것은?

    • A) O(1)
    • B) O(log n)
    • C) O(n)
    • D) O(n²)
  2. 공간 복잡도가 O(1)인 정렬 알고리즘을 2개 이상 말해보세요.

  3. 피보나치 수열을 구하는 재귀 함수의 시간 복잡도를 분석하고, 이를 최적화하는 방법을 제안하세요.

마무리

시간 복잡도와 공간 복잡도는 알고리즘의 효율성을 평가하는 핵심 도구입니다. Big O 표기법을 사용하여 알고리즘을 분석하고 비교할 수 있습니다.

핵심 요약:

  • 시간 복잡도: 알고리즘 실행 시간의 상한
  • 공간 복잡도: 알고리즘 메모리 사용량의 상한
  • Big O: 최고차항만 고려, 계수와 상수 무시
  • 복잡도 클래스: O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)

다음 글에서는 기초 정렬 알고리즘에 대해 알아보겠습니다. 정렬은 실제 프로그래밍에서 가장 많이 사용되는 알고리즘 중 하나입니다!

추가 팁: 알고리즘을 구현할 때는 항상 복잡도를 고려하세요. 작은 입력에서는 차이가 미미하지만, 큰 데이터에서는 성능 차이가 수십 배 이상 날 수 있습니다! 🚀