공간 복잡도와 트레이드오프
호출 스택과 보조 자료 구조에 필요한 보조 공간을 측정하고, 메모이제이션과 제자리 알고리즘의 시간·공간 트레이드오프를 알아봅니다.
공간 복잡도와 트레이드오프은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
공간 복잡도는 무엇을 측정할까요
공간 복잡도는 입력을 제외한 추가 메모리, 즉 보조 공간을 측정합니다. 변수 몇 개만 사용하면 O(1)이고, 결과 배열이나 해시 맵을 사용하면 O(n)입니다. 코드를 확인해 보세요.
# O(1) auxiliary space
def sum_array(nums):
total = 0 # one integer variable
for n in nums:
total += n # constant extra space
return total
# O(n) auxiliary space
def copy_array(nums):
return list(nums) # allocates n slots
print(sum_array([1, 2, 3, 4])) # 10
print(copy_array([1, 2, 3, 4])) # [1, 2, 3, 4]재귀 호출 스택 공간
재귀 호출마다 스택 프레임이 추가되므로 깊이가 공간을 결정합니다. 선형 재귀는 O(n)이고, 균형 잡힌 트리 DFS는 O(log n)입니다. 반복문 버전을 사용하면 이를 더 잘 제어할 수 있습니다.
import sys
def recursive_sum(n):
if n == 0: return 0
return n + recursive_sum(n - 1)
# Space: O(n) stack frames
def iterative_sum(n):
total = 0
while n > 0:
total += n
n -= 1
return total
# Space: O(1)
print(recursive_sum(100)) # 5050
print(iterative_sum(100)) # 5050병합 정렬 공간 복잡도: O(n)
병합 정렬은 임시 배열을 위해 O(n)의 추가 공간이 필요합니다. 이것이 안정적인 O(n log n) 정렬을 위한 비용입니다. 힙 정렬은 공간을 절약하지만 안정적이지 않습니다. 코드를 확인해 보세요.
import tracemalloc
tracemalloc.start()
def merge_sort(arr):
if len(arr) <= 1: return arr
m = len(arr) // 2
l = merge_sort(arr[:m]) # new list
r = merge_sort(arr[m:]) # new list
out, i, j = [], 0, 0
while i < len(l) and j < len(r):
if l[i] <= r[j]: out.append(l[i]); i+=1
else: out.append(r[j]); j+=1
return out + l[i:] + r[j:]
data = list(range(1000, 0, -1))
merge_sort(data)
_, peak = tracemalloc.get_traced_memory()
print(f'Peak memory: {peak} bytes') # proportional to n제자리 알고리즘: O(1) 공간
제자리 알고리즘은 비례해서 증가하는 추가 저장 공간 없이 입력을 직접 변경합니다. 두 포인터로 배열을 뒤집는 것이 한 예입니다. 따라서 공간 복잡도를 O(1)로 유지할 수 있습니다. 코드를 확인해 보세요.
def reverse_inplace(arr):
l, r = 0, len(arr) - 1
while l < r:
arr[l], arr[r] = arr[r], arr[l] # swap
l += 1
r -= 1
# Space: O(1) -- only two pointer variables
def rotate_right(arr, k):
'''Rotate array right by k positions in-place.'''
n = len(arr)
k %= n
arr.reverse() # O(1) space
arr[:k] = arr[:k][::-1]
arr[k:] = arr[k:][::-1]
a = [1, 2, 3, 4, 5]
rotate_right(a, 2)
print(a) # [4, 5, 1, 2, 3]시간-공간 절충: 두 수의 합
시간-공간 절충은 어디에서나 나타납니다. 두 수의 합 문제는 O(1) 공간으로 O(n^2) 시간에 풀거나, 해시 맵을 사용해 O(n) 공간으로 O(n) 시간에 풀 수 있습니다. 두 방법을 모두 언급하고 무엇이 더 중요한지 물어보세요.
# O(n^2) time, O(1) space
def two_sum_slow(nums, target):
for i in range(len(nums)): # O(n)
for j in range(i+1, len(nums)): # O(n)
if nums[i] + nums[j] == target:
return [i, j]
return []
# O(n) time, O(n) space
def two_sum_fast(nums, target):
seen = {} # O(n) space
for i, n in enumerate(nums):
comp = target - n
if comp in seen: # O(1) lookup
return [seen[comp], i]
seen[n] = i
return []
print(two_sum_fast([2, 7, 11, 15], 9)) # [0, 1]메모이제이션과 타뷸레이션의 공간 사용량
하향식 메모이제이션은 O(n)의 메모와 O(n)의 스택 공간을 사용하지만, 상향식 타뷸레이션은 스택을 사용하지 않습니다. 마지막 몇 행만 유지하면 공간을 O(1)로 줄일 수 있는데, 이를 공간 최적화 DP라고 합니다.
# Fibonacci: O(n) space with full table
def fib_table(n):
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# O(1) space: keep only last two values
def fib_optimal(n):
if n <= 1: return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(fib_table(10)) # 55
print(fib_optimal(10)) # 55해시 맵 공간 복잡도: O(n)
해시 맵은 풀이에서 흔히 O(n)의 공간 비용을 만듭니다. 방문한 항목을 기록하는 집합이나 개수를 세는 빈도 맵이 그 예입니다. 항상 이를 밝혀야 합니다. "시간 O(n), 공간 O(n)"이 완전한 답입니다.
def contains_duplicate(nums):
# O(n) time, O(n) space
seen = set()
for n in nums:
if n in seen: return True
seen.add(n)
return False
def group_anagrams(words):
# O(n*m) time, O(n) space (m = avg word length)
from collections import defaultdict
groups = defaultdict(list)
for w in words:
groups[tuple(sorted(w))].append(w)
return list(groups.values())
print(contains_duplicate([1,2,3,1])) # True
print(group_anagrams(['eat','tea','tan','ate','nat','bat']))그래프 알고리즘의 공간 분석
그래프는 실제로 공간을 사용합니다. 인접 리스트는 O(V + E), BFS의 방문 집합과 큐는 O(V), DFS의 재귀 깊이는 O(V)입니다. 그래프의 공간 복잡도는 V와 E를 사용해 표현하세요.
from collections import deque
def bfs(graph, start):
# Space: O(V) for visited set + O(V) for queue
visited = set() # O(V)
queue = deque([start]) # O(V) max
order = []
while queue:
node = queue.popleft()
if node in visited: continue
visited.add(node)
order.append(node)
for nb in graph.get(node, []):
queue.append(nb)
return order
g = {0:[1,2], 1:[3], 2:[3], 3:[]}
print(bfs(g, 0)) # [0, 1, 2, 3]문자열과 배열 할당에서 주의할 점
숨은 메모리 할당으로 O(n)의 공간이 추가될 수 있습니다. 슬라이싱은 새 리스트를 만들고, 반복문에서 문자열에 +를 사용하면 O(n^2)이 됩니다. 정렬 함수는 복사본을 만들지만, lst.sort()는 제자리에서 동작합니다. 코드를 확인해 보세요.
# Hidden allocations:
nums = [1, 2, 3, 4, 5]
# Creates a NEW list -- O(n) space
slice_copy = nums[1:4] # [2, 3, 4]
# Creates a NEW sorted list -- O(n) space
sorted_copy = sorted(nums) # nums unchanged
# Sorts IN PLACE -- O(1) extra space
nums.sort()
print(slice_copy) # [2, 3, 4]
print(sorted_copy) # [1, 2, 3, 4, 5]
print(nums) # [1, 2, 3, 4, 5]면접에서 공간 절충 인식하기
공간 복잡도를 먼저 밝히세요. 면접관이 더 적은 공간을 원한다면 메모이제이션 대신 상향식 DP를 사용하거나, 해시 맵 대신 제자리 정렬을 사용하는 방법이 일반적입니다. 코드를 확인해 보세요.
# Problem: find if array has duplicates
# Option 1: O(1) time-per-check, O(n) space
def has_dup_hash(nums):
return len(nums) != len(set(nums))
# Option 2: O(n log n) time, O(1) extra space
def has_dup_sort(nums):
nums_copy = sorted(nums) # O(n) space -- still!
for i in range(1, len(nums_copy)):
if nums_copy[i] == nums_copy[i-1]:
return True
return False
# Option 3: truly O(1) extra -- sort in-place
def has_dup_inplace(nums):
nums.sort() # modifies original
for i in range(1, len(nums)):
if nums[i] == nums[i-1]: return True
return False전체 복잡도 표기 템플릿
항상 시간과 공간을 포함한 완전한 설명을 제시하세요. "시간은 O(n), 추가 공간은 O(1)입니다."라고 말하면 됩니다. 절충점이 있다면 함께 언급하세요. 이것이 숙련된 지원자를 구분하는 요소입니다.
# Complete complexity example: Merge Intervals
def merge_intervals(intervals):
# Time: O(n log n) for sort + O(n) for merge = O(n log n)
# Space: O(n) for output (could be n/2 to n intervals)
intervals.sort(key=lambda x: x[0]) # O(n log n)
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
print(merge_intervals([[1,3],[2,6],[8,10],[15,18]]))
# [[1,6],[8,10],[15,18]]빠른 확인
빠르게 확인해 보겠습니다. 공간 복잡도에 대한 내용이 얼마나 잘 이해되었는지 살펴보세요. 이제 준비가 되었습니다. ✅
학습 내용 복습
복습해 보겠습니다. 보조 공간은 입력과 별도로 계산하고, 재귀는 깊이에 비례하는 O(depth) 스택 공간을 사용하며, 시간-공간 절충은 대부분의 알고리즘 설계 선택을 좌우합니다.
자주 묻는 질문
“공간 복잡도와 트레이드오프” 강의는 무료인가요?
네 — “공간 복잡도와 트레이드오프” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“공간 복잡도와 트레이드오프”에서 뭘 배우나요?
호출 스택과 보조 자료 구조에 필요한 보조 공간을 측정하고, 메모이제이션과 제자리 알고리즘의 시간·공간 트레이드오프를 알아봅니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“공간 복잡도와 트레이드오프” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Big-O 표기법 기초
- 반복문과 중첩 반복문 분석
- 재귀와 재귀 트리 방법
- 공간 복잡도와 트레이드오프